Model
Managers
AuthManager
❖ 跟上篇文稍稍不同,這次在註冊階段就從 authDataResult 的 user 中取得 uid,並馬上呼叫:
UserManager.shared.createRegData(uid: uid, name: name, email: email, completion: nil)
存到 Firestore。(函式後述)
❖ 未來若要取用 uid 可以直接呼叫下面函式:
func getCurrentUid() -> String? {
return Auth.auth().currentUser?.uid
}
UserManager
❖ 剛剛提到的 createRegData 函式,它把 uid, name, email, createdAt 都存到 Firestore,作為第一批個人資料。
❖ 未來若要更新個人資料,可以把想要更新的欄位做成字典傳入 updateUserInfo 函式。
❖ 讀取個人資料以及讀取其他使用者資料。分別回傳 User? 和 [User]。
技巧是利用 filter 比對每個 user 與登入者的 uid 來篩掉登入者。
PhotoManager
❖ uploadProfileImage 函式負責將個人頭貼上傳到 Storage 下的自訂 path,再從 path 獲得 url 的 absoluteString,將它建立成個人資料的新 key — value pair。(利用剛剛提過的 updateUserInfo 函式)
AppDelegate
這次沒有製作 Loading Screen,且用純程式刻畫面,所以跳轉邏輯要寫在 AppDelegate。
❖ setRootViewController 可以傳入跳轉目標 view controller,並設成 self.window 的 rootViewController,接著 self.window 呼叫 makeKeyAndVisible()。若 animated 為 true 時,要多加下面這行:
UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: nil, completion: nil)
❖ 跳轉邏輯:
如果開 App 發現已登入要跳到 homeNav,未登入則跳到 regNav。
if LocalStorage.shared.hasLoggedIn {
let homeNav = UINavigationController(rootViewController: homeVC)
setRootViewController(homeNav)
} else {
let regNav = UINavigationController(rootViewController: registerVC)
setRootViewController(regNav)
}
❖ 設定 Notification 觀察者:
NotificationCenter.default.addObserver(self, selector: #selector(didLogin), name: K.NotificationName.login, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didLogout), name: K.NotificationName.logout, object: nil)
登入或登出成功時會發送 Notification 給觀察者,接著呼叫 didLogin / didLogout 改變 hasLoggedIn 狀態,並且利用 setRootViewController 函式跳到指定 view controller。
extension AppDelegate {
@objc func didLogin() {
LocalStorage.shared.hasLoggedIn = true
let homeNav = UINavigationController(rootViewController: homeVC)
setRootViewController(homeNav)
}
}extension AppDelegate {
@objc func didLogout() {
LocalStorage.shared.hasLoggedIn = false
let regNav = UINavigationController(rootViewController: registerVC)
setRootViewController(regNav)
}
}
View Controllers
RegisterViewController
❖ signupAndCreateRegData :
為註冊並創造註冊資料的函式,在按下註冊按鈕後呼叫。若註冊成功,要發送 Notification 到剛剛 AppDelegate 內的觀察者呼叫跳轉。
❖ Reactive Programming:
import RxSwift 後,我們將 text fields / buttons / view model 都進行綁定。
- text field 的 driver 為 text,根據 text 驅動 viewModel 的 observer 監控下個送到 observer 的字串 text。(利用
func onNext(_ element: String)
)
2. registerButton 的 driver 為 tap,驅動 signupAndCreateRegData 函式;
3. hadAccountButton 的 driver 為 tap,驅動 push loginVC。
4. viewModel 的 driver 為 validRegisterDriver,根據 validAll 布林值驅動 registerButton 並改變其背景色。
RegiserViewModel
問題:allValid 的值是怎麼得到的?
看看 RegiserViewModel,原來用了 validRegisterSubject(BehaviorSubject<Bool>(value: false)
型別) 及 onNext 方法去監控 allValid 值。
⭐︎ 注意如何用 map、combineLatest、subscribe 去操作 observable sequence。
這邊簡單設定 name 字串至少要一位,email 和 password 字串至少要六位。
LoginViewController
與 RegisterViewController 內容差不多,就不多介紹。
HomeViewController
讀取個人(user) 和讀取他人(users)。
❖ 個人的部分,只要將 user 存成 property 就好。
❖ 他人的部分,把 users 存成 property 後,要確保 cardContainerView 的每個 subview 皆執行了 removeFromSuperview 且 removeConstraints。再將每個 user 匯入 CardView,然後加進 cardContainerView + auto layout。
CardView 介紹
❖ 初始卡片上有的元件。
❖ setup(user: User)
功用是資料顯示及設定 UIPanGestureRecognizer。抓圖部分用 SDWebImage。
❖ 左滑右滑動作設定(panCardView 函式):
往右滑:要拖曳卡片、逆時針轉、‘LIKE’ 標籤顯現。
往左滑:要拖曳卡片、順時針轉、‘NOPE’ 標籤顯現。
結束拖曳:
若 x 平移量大於等於正負 150,卡片要旋轉、掉落、消失。(用到下方的 UIView Extension)
若 x 平移量小於正負 150,要回到原位、‘LIKE’、‘NOPE’ 標籤隱形。
❖ UI
cardImageView 用到自己寫的 CardImageView,裡面是上到下、透明到黑漸變的 gradientLayer,如下程式所示。
CardInfoLabel 也是自己寫的類別,用來產生此畫面的標籤,不提。
class CardImageView: UIImageView {
private let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .darkGray
layer.cornerRadius = 10
contentMode = .scaleAspectFill
clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
gradientLayer.locations = [0.5, 1.0]
layer.addSublayer(gradientLayer)
gradientLayer.frame = self.bounds
}
}
TabControlView 介紹
即底部那排像 Tab Bar 的 View,其實內容就是把 buttons 放到 Stack View。各 buttons 用到下面函式生成,點選後會更換顯示圖片。
func makeTabButton(imageName: String, unselectedImageName: String) -> UIButton {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: imageName), for: .selected)
button.setImage(UIImage(named: unselectedImageName), for: .normal)
button.imageView?.contentMode = .scaleAspectFit
return button
}
點選各個 button,會傳入 button 自己到 handleSelectedButton 函式比對按鈕陣列。若對於 button == selectedButton,則 button.isSelected = true,否則 = false。
ControlView 介紹
即五個 buttons 各自放進一個 view,五個 view 再一起放進一個 stack view。值得注意的是,當按鈕 isHighlighted 為 true(按住不放) 要做縮小動畫,為 false(放開) 要做變回原大小動畫。
按鈕綁定
值得注意的是要用全域變數 isCardAnimating 創造一個執行卡片移除動畫時,不會被再次點選喜歡或不喜歡按鈕而造成出錯的區塊。
ProfileViewController
用傳過來的 user 顯示畫面
編輯區是一個 infoColletionView,裡面有一個 cell(InfoCollectionViewCell)
❖ 使用到一個把 stack view 中的子 stack view 組起來的技巧。而 ProfileLabel 和 ProfileTextField 都是自訂類別,就不提。
❖ infoColletionView:
lazy var infoColletionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.dataSource = self
cv.backgroundColor = .white
cv.isScrollEnabled = false
cv.register(InfoCollectionViewCell.self, forCellWithReuseIdentifier: K.CellID.InfoCollectionViewCell)
return cv
}()
❖ UICollectionViewDataSource:
import RxSwift,把 cell 內每個 text field 的 text 與全域變數(name / age / residence / hobby / introduction / email)做綁定。
extension ProfileViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = infoColletionView.dequeueReusableCell(withReuseIdentifier: K.CellID.InfoCollectionViewCell, for: indexPath) as! InfoCollectionViewCell
cell.user = self.user // user 傳給 cell 的user
setupCellBindings(cell: cell) // 把編輯的內容與全域變數同步
return cell
}private func setupCellBindings(cell: InfoCollectionViewCell) {
cell.nameTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.name = text ?? ""
}
.disposed(by: disposeBag)cell.ageTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.age = text ?? ""
}
.disposed(by: disposeBag)cell.residenceTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.residence = text ?? ""
}
.disposed(by: disposeBag)cell.hobbyTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.hobby = text ?? ""
}
.disposed(by: disposeBag)cell.introductionTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.introduction = text ?? ""
}
.disposed(by: disposeBag)
cell.emailTextField.rx.text
.asDriver()
.drive { [weak self] text in
self?.email = text ?? ""
}
.disposed(by: disposeBag)
}
}
觸發動作
❖ 點選儲存後,要用這些全域變數製作字典並呼叫 updateUserInfo 上傳。如果有用 image picker 換過頭貼了(用全域變數 hasChangedImage 判斷),就呼叫 uploadProfileImage。最後,名字標題也要更新。
❖ 按頭貼編輯按鈕和登出按鈕,前一篇文章做過就不細談。