feat(Views): 增加 profileImageUpload page,增加图片上传功能
增加 profileImageUpload page,增加图片上传功能 Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
parent
698f18624c
commit
3eb9b13f86
@ -36,6 +36,10 @@
|
||||
24A59AF42A010411009C9E3E /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF32A010411009C9E3E /* FirebaseFirestore */; };
|
||||
24A59AF62A010411009C9E3E /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF52A010411009C9E3E /* FirebaseFirestoreSwift */; };
|
||||
24A59AF82A010411009C9E3E /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 24A59AF72A010411009C9E3E /* FirebaseStorage */; };
|
||||
24A59AFA2A01081F009C9E3E /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A59AF92A01081F009C9E3E /* AuthViewModel.swift */; };
|
||||
24FA4D992A012A5D002D202A /* ProfilePhotoSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */; };
|
||||
24FA4D9C2A012E1A002D202A /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D9B2A012E1A002D202A /* ImagePicker.swift */; };
|
||||
24FA4D9E2A0134CB002D202A /* ImageUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FA4D9D2A0134CB002D202A /* ImageUploader.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -66,6 +70,10 @@
|
||||
24A59AE92A00F672009C9E3E /* CustomInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInputField.swift; sourceTree = "<group>"; };
|
||||
24A59AEC2A00F942009C9E3E /* AuthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthHeaderView.swift; sourceTree = "<group>"; };
|
||||
24A59AEE2A010142009C9E3E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
24A59AF92A01081F009C9E3E /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = "<group>"; };
|
||||
24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePhotoSelectorView.swift; sourceTree = "<group>"; };
|
||||
24FA4D9B2A012E1A002D202A /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||
24FA4D9D2A0134CB002D202A /* ImageUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploader.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -102,6 +110,7 @@
|
||||
2492CC0B2A000EB00086C525 /* dudu-tweet */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24FA4D9A2A012E05002D202A /* Utils */,
|
||||
2492CC1F2A0022990086C525 /* Extensions */,
|
||||
2492CC1E2A0022970086C525 /* Model */,
|
||||
2492CC1D2A0022960086C525 /* Service */,
|
||||
@ -144,6 +153,7 @@
|
||||
2492CC1D2A0022960086C525 /* Service */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24FA4D9D2A0134CB002D202A /* ImageUploader.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
@ -353,6 +363,7 @@
|
||||
children = (
|
||||
24A59AE32A00EF1F009C9E3E /* LoginView.swift */,
|
||||
24A59AE52A00EF3A009C9E3E /* RegistrationView.swift */,
|
||||
24FA4D982A012A5D002D202A /* ProfilePhotoSelectorView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -360,6 +371,7 @@
|
||||
24A59AE22A00EEF8009C9E3E /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24A59AF92A01081F009C9E3E /* AuthViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
@ -372,6 +384,14 @@
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
24FA4D9A2A012E05002D202A /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24FA4D9B2A012E1A002D202A /* ImagePicker.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -454,12 +474,15 @@
|
||||
files = (
|
||||
24A59ABE2A003108009C9E3E /* MessagesView.swift in Sources */,
|
||||
24A59AC22A003249009C9E3E /* ProfileView.swift in Sources */,
|
||||
24FA4D9C2A012E1A002D202A /* ImagePicker.swift in Sources */,
|
||||
2492CC0F2A000EB00086C525 /* ContentView.swift in Sources */,
|
||||
2492CC282A0025DD0086C525 /* TweetRowView.swift in Sources */,
|
||||
24FA4D9E2A0134CB002D202A /* ImageUploader.swift in Sources */,
|
||||
24A59AEA2A00F672009C9E3E /* CustomInputField.swift in Sources */,
|
||||
24A59AC42A003A52009C9E3E /* TwewtFilterViewModel.swift in Sources */,
|
||||
2492CC252A0023220086C525 /* FeedView.swift in Sources */,
|
||||
24A59ACE2A00BDCB009C9E3E /* SideMenuView.swift in Sources */,
|
||||
24FA4D992A012A5D002D202A /* ProfilePhotoSelectorView.swift in Sources */,
|
||||
2492CC0D2A000EB00086C525 /* dudu_tweetApp.swift in Sources */,
|
||||
24A59ABA2A0030CB009C9E3E /* ExploreView.swift in Sources */,
|
||||
24A59AD22A00BE14009C9E3E /* SideMenuViewModel.swift in Sources */,
|
||||
@ -469,6 +492,7 @@
|
||||
24A59AE62A00EF3A009C9E3E /* RegistrationView.swift in Sources */,
|
||||
24A59ADD2A00DB9F009C9E3E /* NewTweetView.swift in Sources */,
|
||||
24A59AED2A00F942009C9E3E /* AuthHeaderView.swift in Sources */,
|
||||
24A59AFA2A01081F009C9E3E /* AuthViewModel.swift in Sources */,
|
||||
24A59AE82A00F106009C9E3E /* RoundedShape.swift in Sources */,
|
||||
24A59AC92A00BA81009C9E3E /* UserRowView.swift in Sources */,
|
||||
24A59AB42A002EB8009C9E3E /* MainTabView.swift in Sources */,
|
||||
|
||||
Binary file not shown.
@ -9,11 +9,32 @@ import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var showMenu = false
|
||||
@EnvironmentObject var viewModel: AuthViewModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
// no user loggin
|
||||
if viewModel.userSession == nil {
|
||||
LoginView()
|
||||
} else {
|
||||
// have a logged in user
|
||||
mainInterfaceView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension ContentView {
|
||||
var mainInterfaceView: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
MainTabView()
|
||||
// .navigationBarHidden(showMenu)
|
||||
.toolbar(showMenu ? .hidden : .visible)
|
||||
|
||||
if showMenu {
|
||||
@ -52,9 +73,3 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
//
|
||||
// AuthViewModel.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/2.
|
||||
//
|
||||
|
||||
import Firebase
|
||||
import SwiftUI
|
||||
|
||||
class AuthViewModel: ObservableObject {
|
||||
@Published var userSession: FirebaseAuth.User?
|
||||
@Published var didAuthenticateUser = false
|
||||
private var tempUserSession: FirebaseAuth.User?
|
||||
|
||||
init() {
|
||||
self.userSession = Auth.auth().currentUser
|
||||
|
||||
print("DEBUG: user session is \(self.userSession?.uid)")
|
||||
}
|
||||
|
||||
func login(withEmail email: String, password: String) {
|
||||
print("DEBUG: login with email \(email)")
|
||||
Auth.auth().signIn(withEmail: email, password: password) { result, error in
|
||||
if let error = error {
|
||||
print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let user = result?.user else { return }
|
||||
self.userSession = user
|
||||
|
||||
print("DEBUG: Did log user \(user.uid) in.")
|
||||
}
|
||||
}
|
||||
|
||||
func register(withEmail email: String, password: String, fullname: String, username: String) {
|
||||
print("DEBUG: register with email \(email)")
|
||||
Auth.auth().createUser(withEmail: email, password: password) { result, error in
|
||||
if let error = error {
|
||||
print("DEBUG: Failed to register with error \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let user = result?.user else { return }
|
||||
self.tempUserSession = user
|
||||
|
||||
let data = ["email": email,
|
||||
"username": username.lowercased(),
|
||||
"fullname": fullname,
|
||||
"uid": user.uid]
|
||||
Firestore.firestore().collection("users")
|
||||
.document(user.uid)
|
||||
.setData(data) { _ in
|
||||
print("DEBUG: setData completion block called...")
|
||||
self.didAuthenticateUser = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signOut() {
|
||||
userSession = nil
|
||||
try? Auth.auth().signOut()
|
||||
}
|
||||
|
||||
func uploadProfileImage(_ image: UIImage) {
|
||||
guard let uid = tempUserSession?.uid else { return }
|
||||
|
||||
ImageUploader.uploadImage(image: image) { profileImageUrl in
|
||||
Firestore.firestore().collection("users")
|
||||
.document(uid)
|
||||
.updateData(["profileImageUrl": profileImageUrl]) { _ in
|
||||
self.userSession = self.tempUserSession
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ struct LoginView: View {
|
||||
@State private var email = ""
|
||||
@State private var password = ""
|
||||
|
||||
@EnvironmentObject var viewModel: AuthViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@ -22,6 +23,7 @@ struct LoginView: View {
|
||||
|
||||
CustomInputField(imageName: "lock",
|
||||
palceholderText: "Password",
|
||||
isSecureField: true,
|
||||
text: $password)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
@ -42,7 +44,7 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
Button {
|
||||
print("Sign in here")
|
||||
viewModel.login(withEmail: email, password: password)
|
||||
} label: {
|
||||
Text("Sign In")
|
||||
.font(.headline)
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
//
|
||||
// ProfilePhotoSelectorView.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/2.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePhotoSelectorView: View {
|
||||
@State private var showImagePicker = false
|
||||
@State private var selectedImage: UIImage?
|
||||
@State private var profileImage: Image?
|
||||
@EnvironmentObject var viewModel: AuthViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
AuthHeaderView(title1: "Create your account", title2: "Add a profile photo")
|
||||
|
||||
Button {
|
||||
print("Pick image here..")
|
||||
showImagePicker.toggle()
|
||||
} label: {
|
||||
if let profileImage = profileImage {
|
||||
profileImage
|
||||
.resizable()
|
||||
.modifier(ProfileImageModifier())
|
||||
} else {
|
||||
Image(systemName: "plus.circle")
|
||||
.resizable()
|
||||
.modifier(ProfileImageModifier())
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker, onDismiss: loadImage) {
|
||||
ImagePicker(selectedImage: $selectedImage)
|
||||
}
|
||||
.padding(.top, 44)
|
||||
|
||||
if let selectedImage = selectedImage {
|
||||
Button {
|
||||
print("DEBUG: Finishing register user")
|
||||
viewModel.uploadProfileImage(selectedImage)
|
||||
} label: {
|
||||
Text("Continue")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 340, height: 50)
|
||||
.background(Color(.systemBlue))
|
||||
.clipShape(Capsule())
|
||||
.padding()
|
||||
}
|
||||
.shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
func loadImage() {
|
||||
guard let selectedImage = selectedImage else { return }
|
||||
profileImage = Image(uiImage: selectedImage)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProfileImageModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.foregroundColor(Color(.systemBlue))
|
||||
.scaledToFit()
|
||||
.frame(width: 180, height: 180)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePhotoSelectorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePhotoSelectorView()
|
||||
}
|
||||
}
|
||||
@ -15,12 +15,17 @@ struct RegistrationView: View {
|
||||
|
||||
// @Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject var viewModel: AuthViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
AuthHeaderView(title1: "Get started.",
|
||||
title2: "Create your account")
|
||||
|
||||
NavigationLink(destination: ProfilePhotoSelectorView(),
|
||||
isActive: $viewModel.didAuthenticateUser,
|
||||
label: {})
|
||||
|
||||
VStack(spacing: 40) {
|
||||
CustomInputField(imageName: "envelope",
|
||||
palceholderText: "Email",
|
||||
@ -36,12 +41,16 @@ struct RegistrationView: View {
|
||||
|
||||
CustomInputField(imageName: "lock",
|
||||
palceholderText: "Password",
|
||||
isSecureField: true,
|
||||
text: $password)
|
||||
}
|
||||
.padding(32)
|
||||
|
||||
Button {
|
||||
print("Sign up here")
|
||||
viewModel.register(withEmail: email,
|
||||
password: password,
|
||||
fullname: fullname,
|
||||
username: username)
|
||||
} label: {
|
||||
Text("Sign Up")
|
||||
.font(.headline)
|
||||
|
||||
@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct CustomInputField: View {
|
||||
let imageName: String
|
||||
let palceholderText: String
|
||||
var isSecureField: Bool? = false
|
||||
@Binding var text: String
|
||||
|
||||
|
||||
@ -21,8 +22,12 @@ struct CustomInputField: View {
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(Color(.darkGray))
|
||||
if isSecureField ?? false {
|
||||
SecureField(palceholderText, text: $text)
|
||||
} else {
|
||||
TextField(palceholderText, text: $text)
|
||||
}
|
||||
|
||||
TextField(palceholderText, text: $text)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SideMenuView: View {
|
||||
|
||||
@EnvironmentObject var authViewModel: AuthViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 32) {
|
||||
VStack(alignment: .leading) {
|
||||
@ -36,7 +39,7 @@ struct SideMenuView: View {
|
||||
}
|
||||
} else if viewModel == .logout {
|
||||
Button {
|
||||
print("handle logout here")
|
||||
authViewModel.signOut()
|
||||
} label: {
|
||||
SideMenuOptionRowView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
31
dudu-tweet/dudu-tweet/Service/ImageUploader.swift
Normal file
31
dudu-tweet/dudu-tweet/Service/ImageUploader.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// ImageUploader.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/2.
|
||||
//
|
||||
|
||||
import Firebase
|
||||
import FirebaseStorage
|
||||
import UIKit
|
||||
|
||||
struct ImageUploader {
|
||||
static func uploadImage(image: UIImage, completion: @escaping (String) -> Void) {
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.5) else { return }
|
||||
|
||||
let filename = NSUUID().uuidString
|
||||
let ref = Storage.storage().reference(withPath: "/profile_image/\(filename)")
|
||||
|
||||
ref.putData(imageData, metadata: nil) { _, error in
|
||||
if let error = error {
|
||||
print("DEBUG: Failed to upload image with error \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
ref.downloadURL { imageUrl, _ in
|
||||
guard let imageUrl = imageUrl?.absoluteURL else { return }
|
||||
completion(imageUrl.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
dudu-tweet/dudu-tweet/Utils/ImagePicker.swift
Normal file
44
dudu-tweet/dudu-tweet/Utils/ImagePicker.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// ImagePicker.swift
|
||||
// dudu-tweet
|
||||
//
|
||||
// Created by ching on 2023/5/2.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
@Binding var selectedImage: UIImage?
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(self)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension ImagePicker {
|
||||
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
|
||||
let parent: ImagePicker
|
||||
|
||||
init(_ parent: ImagePicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
guard let image = info[.originalImage] as? UIImage else { return }
|
||||
parent.selectedImage = image
|
||||
parent.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,9 @@ import Firebase
|
||||
@main
|
||||
struct dudu_tweetApp: App {
|
||||
|
||||
@StateObject var viewModel = AuthViewModel()
|
||||
|
||||
|
||||
init() {
|
||||
FirebaseApp.configure()
|
||||
}
|
||||
@ -19,8 +22,9 @@ struct dudu_tweetApp: App {
|
||||
WindowGroup {
|
||||
NavigationView {
|
||||
ContentView()
|
||||
// LoginView()
|
||||
// ProfilePhotoSelectorView()
|
||||
}
|
||||
.environmentObject(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user