인스타그램 만들기 v0.1

인스타그램 핵심 기능을 구현해보자.

기능 Spec

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

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

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




문제 상황 및 해결 과정

✔︎ 문제 상황

✔︎ 해결 과정

  1. 새로운 유저가 회원가입을 하면, Firebase.Auth 를 통해서 uid/email을 받아 User(uid:,email:)를 init 하여 GlobalState.shared.currentUser 에 넣었다.
  2. NotificationCenter를 통해 userLogined를 알린다.
  3. 유저 로그인에 따른 ui 업데이트가 필요한 모든 뷰콘트롤러 또는 커스텀cell들은 모두 각자의 클래스 내에서 currentUser: User? 를 갖고 있으며, NotificationCenter의 userLogined 노티를 구독한다. 노티가 있을 때마다 GlobalState.shared.currentUser를 다시 불러와서(self.currentUser = GlobalState.shared.currentUser) ui를 재정비(self.collectionView.reloadData())한다.
  4. 어떤 메소드 내에서 유저가 프로필 이미지를 바꾸거나 닉네임을 바꾸는 등 currentUser의 내부 프로퍼티 정보가 업데이트 되는 경우에는 GlobalState.shared.currentUser의 프로퍼티를 우선 수정(GlobalState.shared.currentUser.profileImageUrl = imageUrl)하게 한다음에, NotificationCenter의 userInfoChanged 노티를 주고, 나머지 뷰콘이나 cell들은 또 userInfoChanged 노티를 구독하여 유저 프로퍼티의 변화가 있을 때 콜렉션뷰를 리로드 하거나 이미지뷰의 이미지를 리로드 했다.

✔︎ 찜찜한 점