Written by
Amy
on
on
인스타그램 만들기 v0.1
인스타그램 핵심 기능을 구현해보자.
기능 Spec
- Firebase 서버를 이용한 로그인, 회원가입, DB 관리
- 핵심기능1. 로그인/회원가입
- 핵심기능2. 이미지 + 텍스트 함께 포스팅
- 핵심기능3. 포스팅한 내용을 그리드뷰/리스트뷰로 피드 제공
- 부가기능1. 유저 프로필 수정 및 포스트 라이크 기능
- 부가기능2. 코멘트 남기기
User Model
struct User {
let uid: String
let email: String
var profileImageUrl: String?
init(uid: String, email: String) {
self.uid = uid
self.email = email
}
}
Post Model
struct Post {
let uid: String
var key: String?
let imageUrl: String
let caption: String
let creationDate: Date
var hasLiked = false
init(uid: String, dictionary: [String: Any]) {
self.uid = uid
self.imageUrl = dictionary["imageUrl"] as? String ?? ""
self.caption = dictionary["caption"] as? String ?? ""
let secondsFrom1970 = dictionary["creationDate"] as? Double ?? 0
self.creationDate = Date(timeIntervalSince1970: secondsFrom1970)
}
}
App, Api, FirebaseApi
- 유저를 로드하거나, 포스트를 로드하는 등 Firebase 서버와 통신해야 하는 메소드들은 FirebaseApi로 분리했다.
import Foundation
import Firebase
struct App {
static var api: API = FirebaseAPI()
}
protocol API {
typealias SuccessHandler = (_ isSuccess: Bool) -> Void
typealias FetchPostsHandler = ([Post]) -> Void
typealias FetchCommentsHandler = (User?) -> Void
typealias FetchFollowersHandler = (User?) -> Void
func saveUser(uid: String, email: String) -> Void
func fetchUser(handler: @escaping SuccessHandler)
func fetchPosts(uid: String, handler: @escaping FetchPostsHandler) -> Void
func uploadProfileImage(imageData: Data, handler: @escaping (_ isSuccess: Bool) -> Void )
}
class FirebaseAPI: API {
let baseReference = Database.database().reference()
func fetchPosts(uid: String, handler: @escaping FetchPostsHandler) {
baseReference.child(GlobalState.Constants.posts.rawValue)
.child(uid)
.observeSingleEvent(of: .value) { (snapshot) in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var posts = [Post]()
dictionaries.forEach({ (key, value) in
guard let dictionary = value as? [String: Any] else { return }
var post = Post(uid: uid , dictionary: dictionary)
post.key = key
posts.append(post)
self.baseReference.child("likes")
.child(key)
.child(uid)
.observeSingleEvent(of: .value, with: { (snapshot) in
if let value = snapshot.value as? Int, value == 1 {
post.hasLiked = true
} else {
post.hasLiked = false
}
})
})
DispatchQueue.main.async {
handler(posts)
}
}
}
func saveUser(uid: String, email: String) {
let values = ["uid":uid,"email":email]
baseReference.child(GlobalState.Constants.users.rawValue)
.child(uid)
.updateChildValues(values) { (err, ref) in
if let err = err {
print("Failed to save user to DB", err)
return
}
print("Successfully saved user to DB")
}
}
func fetchUser(handler: @escaping (_ isSuccess: Bool) -> Void) {
guard let uid = GlobalState.instance.uid else { return }
baseReference.child(GlobalState.Constants.users.rawValue)
.child(uid)
.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userDictionary = snapshot.value as? [String: Any] else { return }
let email = userDictionary["email"] as! String
let profileImageUrl = userDictionary["profileImageUrl"] as? String
var user = User(uid: uid, email: email)
user.profileImageUrl = profileImageUrl
GlobalState.instance.user = user
handler(true)
})
}
func uploadProfileImage(imageData: Data, handler: @escaping (_ isSuccess: Bool) -> Void) {
let filename = NSUUID().uuidString
Storage.storage().reference()
.child(GlobalState.Constants.users.rawValue)
.child(filename)
.putData(imageData, metadata: nil) { (metadata, err) in
// Storage Failed
if let err = err {
print("Failed to upload post image:", err)
handler(false)
return
}
// Storage success
guard let imageUrl = metadata?.downloadURL()?.absoluteString else { return }
guard let uid = GlobalState.instance.uid else { return }
NotificationCenter.default.post(name: Notification.Name.uploadProfileImage,
object: imageUrl)
let values = ["profileImageUrl": imageUrl]
self.baseReference.child(GlobalState.Constants.users.rawValue)
.child(uid)
.updateChildValues(values) { (err, ref) in
// DB Failed
if let err = err {
print("Failed to saved profile image to DB", err)
handler(false)
return
}
// DB success
handler(true)
print("Successfully saved profile image to DB")
}
}
}
}
GlobalState
- Firebase Auth로 인증된 유저의 uid, email, profileImageUrl 은 UserDefaults에 computed property로 저장해놓고, GlobalState가 user를 싱글톤으로 관리하도록 설계했다.
import Foundation
final class GlobalState {
static var instance = GlobalState()
private init() {
loadUser()
NotificationCenter.default
.addObserver(forName:
NSNotification.Name.uploadProfileImage,
object: nil,
queue: nil) { (noti) in
guard let newUrl = noti.object as? String else { return }
GlobalState.instance.user?.profileImageUrl = newUrl
GlobalState.instance.profileImageUrl = newUrl
NotificationCenter.default
.post(name: NSNotification.Name.userUpdatedInfo, object: nil)
}
}
enum Constants: String {
case uid
case email
case profileImageUrl
case users
case posts
case comments
case following
case likes
}
func loadUser() {
guard let uid = uid, let email = email else { return }
var newUser = User(uid: uid, email: email)
newUser.profileImageUrl = profileImageUrl
user = newUser
}
var user: User?
var isLoggedin: Bool {
let isEmpty = uid?.isEmpty ?? true
return !isEmpty
}
var uid: String? {
get {
let uid = UserDefaults.standard.string(
forKey: Constants.uid.rawValue)
return uid
}set {
UserDefaults.standard.set(newValue, forKey: Constants.uid.rawValue)
}
}
var email: String? {
get {
let email = UserDefaults.standard.string(
forKey: Constants.email.rawValue)
return email
}set {
UserDefaults.standard.set(newValue, forKey: Constants.email.rawValue)
}
}
var profileImageUrl: String? {
get {
let profileImageUrl = UserDefaults.standard.string(
forKey: Constants.profileImageUrl.rawValue)
return profileImageUrl
}set {
UserDefaults.standard.set(newValue, forKey: Constants.profileImageUrl.rawValue)
}
}
}
User Model
struct User {
let uid: String
let email: String
var profileImageUrl: String?
init(uid: String, email: String) {
self.uid = uid
self.email = email
}
}
Post Model
struct Post {
let uid: String
var key: String?
let imageUrl: String
let caption: String
let creationDate: Date
var hasLiked = false
init(uid: String, dictionary: [String: Any]) {
self.uid = uid
self.imageUrl = dictionary["imageUrl"] as? String ?? ""
self.caption = dictionary["caption"] as? String ?? ""
let secondsFrom1970 = dictionary["creationDate"] as? Double ?? 0
self.creationDate = Date(timeIntervalSince1970: secondsFrom1970)
}
}
문제 상황 및 해결 과정
✔︎ 문제 상황
- 로그인한 유저의 정보에 따라서 앱의 모든 페이지에서 ui를 업데이트 해야 할 일이 많아진다.
✔︎ 해결 과정
- 새로운 유저가 회원가입을 하면, Firebase.Auth 를 통해서 uid/email을 받아 User(uid:,email:)를 init 하여 GlobalState.shared.currentUser 에 넣었다.
- NotificationCenter를 통해 userLogined를 알린다.
- 유저 로그인에 따른 ui 업데이트가 필요한 모든 뷰콘트롤러 또는 커스텀cell들은 모두 각자의 클래스 내에서 currentUser: User? 를 갖고 있으며, NotificationCenter의 userLogined 노티를 구독한다. 노티가 있을 때마다 GlobalState.shared.currentUser를 다시 불러와서(self.currentUser = GlobalState.shared.currentUser) ui를 재정비(self.collectionView.reloadData())한다.
- 어떤 메소드 내에서 유저가 프로필 이미지를 바꾸거나 닉네임을 바꾸는 등 currentUser의 내부 프로퍼티 정보가 업데이트 되는 경우에는 GlobalState.shared.currentUser의 프로퍼티를 우선 수정(GlobalState.shared.currentUser.profileImageUrl = imageUrl)하게 한다음에, NotificationCenter의 userInfoChanged 노티를 주고, 나머지 뷰콘이나 cell들은 또 userInfoChanged 노티를 구독하여 유저 프로퍼티의 변화가 있을 때 콜렉션뷰를 리로드 하거나 이미지뷰의 이미지를 리로드 했다.
✔︎ 찜찜한 점
- 로그인한 유저의 정보를 UI에 반영해야 하는 콜렉션 뷰콘트롤러와 커스텀 cell이 다수일 경우 이 모든 뷰콘과 cell들이 자기 클래스 내에서 currentUser를 독자적으로 가지고 있고 이 currentUser가 싱글톤 클래스의 user를 바라보고 있으며, 업데이트가 일어날 때마다 NotificationCenter를 통해 계속해서 알림을 받고 콜렉션뷰를 리로드를 해주는 것이 맞는 방법이 가장 효율적인지 아직 의문이 남는다.