feat(Views): 增加 profileImageUpload page,增加图片上传功能

增加 profileImageUpload page,增加图片上传功能

Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
Ching 2023-05-02 20:34:49 +08:00
parent 698f18624c
commit 3eb9b13f86
12 changed files with 308 additions and 12 deletions

View File

@ -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 */,

View File

@ -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()
}
}

View File

@ -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
}
}
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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)

View File

@ -10,6 +10,7 @@ import SwiftUI
struct CustomInputField: View {
let imageName: String
let palceholderText: String
var isSecureField: Bool? = false
@Binding var text: String
@ -21,10 +22,14 @@ struct CustomInputField: View {
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundColor(Color(.darkGray))
if isSecureField ?? false {
SecureField(palceholderText, text: $text)
} else {
TextField(palceholderText, text: $text)
}
}
Divider()
.background(Color(.darkGray))
}

View File

@ -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)
}

View 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)
}
}
}
}

View 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()
}
}
}

View File

@ -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)
}
}
}