Written by
Amy
on
on
인스타그램 만들기 v0.4
인스타그램 핵심 기능을 구현해보자.
기능 Spec
- Firebase 서버를 이용한 로그인, 회원가입, DB 관리
- 핵심기능1. 로그인/회원가입
- 핵심기능2. 이미지 + 텍스트 함께 포스팅
- 핵심기능3. 포스팅한 내용을 그리드뷰/리스트뷰로 피드 제공
- 부가기능1. 유저 프로필 수정 및 포스트 라이크 기능
- 부가기능2. 코멘트 남기기
PostController
- 커스텀한 이미지피커를 띄워, 사진을 선택하면 글을 쓰는 뷰콘트롤러가 푸시되고, Share 버튼을 누르면 최종적으로 DB에도 올라가고 메인 콘트롤러도 업데이트되는 구조이다.
import UIKit
import Firebase
class PostController: UIViewController {
var selectedImage: UIImage? {
didSet {
self.imageView.image = selectedImage
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.lightGray
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(handleShare))
setupImageAndTextViews()
}
let imageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .red
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
let textView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 14)
return tv
}()
fileprivate func setupImageAndTextViews() {
let containerView = UIView()
containerView.backgroundColor = .white
view.addSubview(containerView)
containerView.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 100)
containerView.addSubview(imageView)
imageView.anchor(top: containerView.topAnchor, left: containerView.leftAnchor, bottom: containerView.bottomAnchor, right: nil, paddingTop: 8, paddingLeft: 8, paddingBottom: 8, paddingRight: 0, width: 84, height: 0)
containerView.addSubview(textView)
textView.anchor(top: containerView.topAnchor, left: imageView.rightAnchor, bottom: containerView.bottomAnchor, right: containerView.rightAnchor, paddingTop: 0, paddingLeft: 4, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
@objc func handleShare() {
guard let caption = textView.text, !caption.isEmpty else { return }
guard let image = selectedImage else { return }
guard let uploadData = UIImageJPEGRepresentation(image, 0.5) else { return }
navigationItem.rightBarButtonItem?.isEnabled = false // Upload 할 때 share 버튼 enabled 막기
let filename = NSUUID().uuidString // filename => Random String
Storage.storage().reference().child("posts").child(filename).putData(uploadData, metadata: nil) { (metadata, err) in
if let err = err {
self.navigationItem.rightBarButtonItem?.isEnabled = true
print("Failed to upload post image:", err)
return
}
guard let imageUrl = metadata?.downloadURL()?.absoluteString else { return }
self.saveToDatabaseWithImageUrl(imageUrl: imageUrl)
}
}
fileprivate func saveToDatabaseWithImageUrl(imageUrl: String) {
guard let postImage = selectedImage else { return }
guard let caption = textView.text else { return }
guard let uid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("posts").child(uid).childByAutoId()
let values = ["imageUrl": imageUrl, "caption": caption, "imageWidth": postImage.size.width, "imageHeight": postImage.size.height, "creationDate": Date().timeIntervalSince1970] as [String : Any]
ref.updateChildValues(values) { (err, ref) in
if let err = err {
self.navigationItem.rightBarButtonItem?.isEnabled = true
print("Failed to save post to DB", err)
return
}
print("Successfully saved post to DB")
self.dismiss(animated: true, completion: nil)
NotificationCenter.default.post(name: Notification.Name.newPost, object: nil)
}
}
override var prefersStatusBarHidden: Bool {
return true
}
}
PostGridCell
class PostGridCell: UICollectionViewCell {
var post: Post? {
didSet {
guard let imageUrl = post?.imageUrl else { return }
photoImageView.loadImage(URLstring: imageUrl)
}
}
let photoImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(photoImageView)
photoImageView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
PostListCell
import UIKit
protocol PostListCellDelegate {
func didTapComment(post: Post)
func didLike(for cell: PostListCell)
}
class PostListCell: UICollectionViewCell {
var delegate: PostListCellDelegate?
var post: Post? {
didSet {
usernameLabel.text = "UserName"
likeButton.setImage(post?.hasLiked == true ? #imageLiteral(resourceName: "like_selected").withRenderingMode(.alwaysOriginal) : #imageLiteral(resourceName: "like_unselected").withRenderingMode(.alwaysOriginal), for: .normal)
if let postImageUrl = post?.imageUrl {
photoImageView.loadImage(URLstring: postImageUrl)
}
fileprivate func setupAttributedCaption() {
guard let post = self.post else { return }
let attributedText = NSMutableAttributedString(string: "\(post.uid)\n", attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 14)])
attributedText.append(NSAttributedString(string: "\(post.caption)", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14)]))
attributedText.append(NSAttributedString(string: "\n\n", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 4)]))
let timeAgoDisplay = post.creationDate.timeAgoDisplay()
attributedText.append(NSAttributedString(string: timeAgoDisplay, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14), NSAttributedStringKey.foregroundColor: UIColor.gray]))
captionLabel.attributedText = attributedText
}
let userProfileImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.backgroundColor = .lightGray
iv.clipsToBounds = true
return iv
}()
let photoImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
let usernameLabel: UILabel = {
let label = UILabel()
label.text = "Username"
label.font = UIFont.boldSystemFont(ofSize: 14)
return label
}()
let optionsButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("•••", for: .normal)
button.setTitleColor(.black, for: .normal)
return button
}()
lazy var likeButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(#imageLiteral(resourceName: "like_unselected").withRenderingMode(.alwaysOriginal), for: .normal)
button.addTarget(self, action: #selector(handleLike), for: .touchUpInside)
return button
}()
@objc func handleLike() {
print("Handling like from within cell...")
delegate?.didLike(for: self)
}
lazy var commentButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(#imageLiteral(resourceName: "comment").withRenderingMode(.alwaysOriginal), for: .normal)
button.addTarget(self, action: #selector(handleComment), for: .touchUpInside)
return button
}()
@objc func handleComment() {
print("Trying to show comments...")
guard let post = post else { return }
delegate?.didTapComment(post: post)
}
let captionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(userProfileImageView)
addSubview(usernameLabel)
addSubview(optionsButton)
addSubview(photoImageView)
userProfileImageView.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 8, paddingLeft: 8, paddingBottom: 0, paddingRight: 0, width: 40, height: 40)
userProfileImageView.layer.cornerRadius = 40 / 2
usernameLabel.anchor(top: topAnchor, left: userProfileImageView.rightAnchor, bottom: photoImageView.topAnchor, right: optionsButton.leftAnchor, paddingTop: 0, paddingLeft: 8, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
optionsButton.anchor(top: topAnchor, left: nil, bottom: photoImageView.topAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 44, height: 0)
photoImageView.anchor(top: userProfileImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: 8, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
photoImageView.heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1).isActive = true
setupActionButtons()
addSubview(captionLabel)
captionLabel.anchor(top: likeButton.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 8, paddingBottom: 0, paddingRight: 8, width: 0, height: 0)
}
fileprivate func setupActionButtons() {
let stackView = UIStackView(arrangedSubviews: [likeButton, commentButton])
stackView.distribution = .fillEqually
addSubview(stackView)
stackView.anchor(top: photoImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 4, paddingBottom: 0, paddingRight: 0, width: 60, height: 40)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}