Written by
Amy
on November 21, 2017
인스타그램 만들기 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를 통해 계속해서 알림을 받고 콜렉션뷰를 리로드를 해주는 것이 맞는 방법이 가장 효율적인지 아직 의문이 남는다.