Written by
Amy
on
on
인스타그램 만들기 v0.3
인스타그램 핵심 기능을 구현해보자. Home 컨트롤러에서는 유저의 프로필과 포스트들이 전시된다.
기능 Spec
- Firebase 서버를 이용한 로그인, 회원가입, DB 관리
- 핵심기능1. 로그인/회원가입
- 핵심기능2. 이미지 + 텍스트 함께 포스팅
- 핵심기능3. 포스팅한 내용을 그리드뷰/리스트뷰로 피드 제공
- 부가기능1. 유저 프로필 수정 및 포스트 라이크 기능
- 부가기능2. 코멘트 남기기
MyHomeHeader
- 유저의 프로필을 전시할 HeaderCell을 커스텀 했다. 유저를 받아 UI를 업데이트하는 구조이다.
internal class MyHomeHeader : UICollectionViewCell {
internal var delegate: MyHomeHeaderDelegate?
internal var user: Onstagram.User? { get set }
internal var headerBtnType: HeaderBtnType
lazy internal var editProfileFollowButton: UIButton { get set }
@objc internal func handleEditProfileOrFollow()
internal var profileImageView: UIImageView
lazy internal var gridButton: UIButton { get set }
@objc internal func handleChangeToGridView()
lazy internal var listButton: UIButton { get set }
@objc internal func handleChangeToListView()
internal let bookmarkButton: UIButton
internal let usernameLabel: UILabel
internal let postsLabel: UILabel
internal let followersLabel: UILabel
internal let followingLabel: UILabel
override internal init(frame: CGRect)
required internal init?(coder aDecoder: NSCoder)
}
var user: User? {
didSet {
usernameLabel.text = user?.email
setupHeaderBtn()
guard let profileImageUrl = user?.profileImageUrl else { return }
profileImageView.loadImage(URLstring: profileImageUrl)
}
}
MyHomeHeaderDelegate
- 버튼에 대한 액션을 처리하기 위해 델리게이트 패턴을 사용했다.
internal protocol MyHomeHeaderDelegate {
internal func didChangeToListView()
internal func didChangeToGridView()
internal func didTapEditProfileBtn()
}
HeaderBtnType
- 버튼 타입에 따라 기능을 분기해야 했기 때문에 enum을 사용했다.
enum HeaderBtnType: String {
case follow = "Follow"
case unfollow = "Unfollow"
case editProfile = "Edit Profile"
case loading = "Now Loading..."
}
var headerBtnType: HeaderBtnType = .loading
lazy var editProfileFollowButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle(headerBtnType.rawValue, for: .normal)
button.setTitleColor(.black, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
button.layer.borderColor = UIColor.lightGray.cgColor
button.layer.borderWidth = 1
button.layer.cornerRadius = 3
button.addTarget(self, action: #selector(handleEditProfileOrFollow), for: .touchUpInside)
return button
}()
fileprivate func setupHeaderBtn() {
guard let currentLoggedInUserId = Auth.auth().currentUser?.uid else { return }
guard let userId = user?.uid else { return }
if currentLoggedInUserId == userId {
// edit profile
self.headerBtnType = .editProfile
self.editProfileFollowButton.setTitle(headerBtnType.rawValue, for: .normal)
} else {
// check if following
Database.database().reference().child("following")
.child(currentLoggedInUserId)
.child(userId)
.observeSingleEvent(of: .value, with: { (snapshot) in
if let isFollowing = snapshot.value as? Int, isFollowing == 1 {
self.updateUnFollowBtn()
} else {
self.updateFollowBtn()
}
}, withCancel: { (err) in
print("Failed to check if following:", err)
})
}
}
@objc func handleEditProfileOrFollow() {
guard let currentLoggedInUserId = Auth.auth().currentUser?.uid else { return }
guard let userId = user?.uid else { return }
switch headerBtnType {
case .editProfile:
delegate?.didTapEditProfileBtn()
case .follow:
updateUnFollowBtn()
let ref = Database.database().reference().child("following").child(currentLoggedInUserId)
let values = [userId: 1]
ref.updateChildValues(values) { (err, ref) in
if let err = err {
print("Failed to follow user:", err)
return
}
}
case .unfollow:
updateFollowBtn()
Database.database().reference().child("following")
.child(currentLoggedInUserId)
.child(userId)
.removeValue(completionBlock: { (err, ref) in
if let err = err {
print("Failed to unfollow user:", err)
}
})
case .loading:
break
}
}
fileprivate func updateFollowBtn() {
self.headerBtnType = .follow
self.editProfileFollowButton.setTitle(self.headerBtnType.rawValue, for: .normal)
self.editProfileFollowButton.backgroundColor = .blue
self.editProfileFollowButton.setTitleColor(.white, for: .normal)
self.editProfileFollowButton.layer.borderColor = UIColor(white: 0, alpha: 0.2).cgColor
}
fileprivate func updateUnFollowBtn() {
self.headerBtnType = .unfollow
self.editProfileFollowButton.setTitle(self.headerBtnType.rawValue, for: .normal)
self.editProfileFollowButton.backgroundColor = .white
self.editProfileFollowButton.setTitleColor(.black, for: .normal)
}
MyHomeController
- 헤더를 가지고 있는 홈 뷰콘트롤러가 delegate가 되어, 헤더의 버튼 기능들을 처리한다.
extension MyHomeController: MyHomeHeaderDelegate {
// MARK: - MyHomeHeaderDelegate
func didChangeToGridView() {
isGridView = true
collectionView?.reloadData()
}
func didChangeToListView() {
isGridView = false
collectionView?.reloadData()
}
func didTapEditProfileBtn() {
let profileImagePicker = PhotoSelectorController()
profileImagePicker.pickerType = .ProfileImagePicker
profileImagePicker.delegate = self
let pickerNavi = UINavigationController(rootViewController: profileImagePicker)
self.present(pickerNavi, animated: true, completion: nil)
}
}
MyHomeController
- 노티를 구독하여 콜렉션뷰를 업데이트 하는 역할을 한다.
NotificationCenter.default.addObserver(self,
selector: #selector(fetchPosts),
name: Notification.Name.newPost,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(fetchUser),
name: NSNotification.Name.userLogined,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(needUpdateHeader),
name: NSNotification.Name.userUpdatedInfo,
object: nil)
@objc fileprivate func fetchPosts() {
guard let user = self.currentUser else { return }
self.posts.removeAll()
App.api.fetchPosts(uid: user.uid) { (posts) in
self.posts = posts
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
}
}
@objc fileprivate func fetchUser() {
self.currentUser = GlobalState.instance.user
self.fetchPosts()
}
@objc fileprivate func needUpdateHeader() {
self.currentUser = GlobalState.instance.user
collectionView?.reloadData()
}