Build Instagram App: Part 9 (Swift 5) - 2020 - Xcode 11 - iOS Development
enum Output {
case postButtonDidTap
case follwerButtonDidTap
case followingButtonDidTap
case editButtonDidTap
}
private func bind() {
follwerButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.follwerButtonDidTap)
}
.store(in: &cancellabels)
followingButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.followingButtonDidTap)
}
.store(in: &cancellabels)
editProfileButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.editButtonDidTap)
}
.store(in: &cancellabels)
postButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.postButtonDidTap)
}
.store(in: &cancellabels)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() }
if indexPath.section == 0 {
guard let profileHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier, for: indexPath) as? ProfileInfoHeaderCollectionReusableView else { return UICollectionReusableView() }
if outputSubscription == nil {
let output = profileHeader.transform()
bind(in: output)
}
return profileHeader
} else {
guard let tabControllHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier, for: indexPath) as? ProfileTabsCollectionReusableView else { return UICollectionReusableView() }
return tabControllHeader
}
}
extension ProfileViewController {
private func bind(in output: AnyPublisher<ProfileInfoHeaderCollectionReusableView.Output, Never>) {
outputSubscription = output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .postButtonDidTap:
self?.collectionView?.scrollToItem(at: IndexPath(row: 0, section: 1), at: .top, animated: true)
case .editButtonDidTap:
let vc = EditProfileViewController()
vc.title = "Edit Profile"
let navVC = UINavigationController(rootViewController: vc)
navVC.modalPresentationStyle = .fullScreen
self?.present(navVC, animated: true)
case .followingButtonDidTap:
let vc = ListViewController()
vc.title = "Follwing"
vc.navigationItem.largeTitleDisplayMode = .never
self?.navigationController?.pushViewController(vc, animated: true)
case .follwerButtonDidTap:
let vc = ListViewController()
vc.title = "Follwers"
vc.navigationItem.largeTitleDisplayMode = .never
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
}
import UIKit
import Combine
class ProfileViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private var outputSubscription: AnyCancellable?
private let viewModel = ProfileViewModel()
private var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
setUI()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
private func setUI() {
view.backgroundColor = .systemBackground
setNavigationBar()
setCollectionView()
}
private func setCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)
layout.minimumLineSpacing = 1
layout.minimumInteritemSpacing = 1
let size = (view.width - 4) / 3
layout.itemSize = CGSize(width: size, height: size)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.register(PhotoCollectionViewCell.self, forCellWithReuseIdentifier: PhotoCollectionViewCell.identifier)
collectionView?.register(ProfileInfoHeaderCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier)
collectionView?.register(ProfileTabsCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier)
guard let collectionView = collectionView else { return }
view.addSubview(collectionView)
}
private func setNavigationBar() {
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "gear")?.withTintColor(UIColor.black, renderingMode: .alwaysOriginal), style: .done, target: self, action: #selector(settingButtonDidTap))
}
@objc private func settingButtonDidTap() {
let vc = SettingsViewController()
vc.title = "Settings"
navigationController?.pushViewController(vc, animated: true)
}
}
extension ProfileViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
let model = viewModel.userPostsModel.value[indexPath.row]
let vc = PostViewController(model: model)
vc.title = "Post"
vc.navigationItem.largeTitleDisplayMode = .never
navigationController?.pushViewController(vc, animated: true)
}
}
extension ProfileViewController: UICollectionViewDelegateFlowLayout {
}
extension ProfileViewController {
private func bind(in output: AnyPublisher<ProfileInfoHeaderCollectionReusableView.Output, Never>) {
outputSubscription = output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .postButtonDidTap:
self?.collectionView?.scrollToItem(at: IndexPath(row: 0, section: 1), at: .top, animated: true)
case .editButtonDidTap:
let vc = EditProfileViewController()
vc.title = "Edit Profile"
let navVC = UINavigationController(rootViewController: vc)
navVC.modalPresentationStyle = .fullScreen
self?.present(navVC, animated: true)
case .followingButtonDidTap:
let vc = ListViewController()
vc.title = "Follwing"
vc.navigationItem.largeTitleDisplayMode = .never
self?.navigationController?.pushViewController(vc, animated: true)
case .follwerButtonDidTap:
let vc = ListViewController()
vc.title = "Follwers"
vc.navigationItem.largeTitleDisplayMode = .never
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
}
extension ProfileViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() }
if indexPath.section == 0 {
guard let profileHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileInfoHeaderCollectionReusableView.identifier, for: indexPath) as? ProfileInfoHeaderCollectionReusableView else { return UICollectionReusableView() }
if outputSubscription == nil {
let output = profileHeader.transform()
bind(in: output)
}
return profileHeader
} else {
guard let tabControllHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileTabsCollectionReusableView.identifier, for: indexPath) as? ProfileTabsCollectionReusableView else { return UICollectionReusableView() }
return tabControllHeader
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if section == 0 {
return CGSize(width: collectionView.width, height: collectionView.height / 3)
} else {
return CGSize(width: collectionView.width, height: 65)
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 0
} else {
return 100
// return viewModel.userPostsModel.value.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.identifier, for: indexPath) as? PhotoCollectionViewCell else { return UICollectionViewCell() }
// let model = viewModel.userPostsModel.value[indexPath.row]
// cell.configure(with: model)
if let image = UIImage(named: "background") {
cell.configure(with: image)
}
return cell
}
}
import Foundation
import Combine
class ProfileViewModel {
let userPostsModel: CurrentValueSubject<[UserPostModel], Never> = .init([])
private var cancellables = Set<AnyCancellable>()
init() {
addSubscription()
}
private func addSubscription() {
fetchMockData()
}
private func fetchMockData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.userPostsModel.send([])
}
}
}
import UIKit
import Combine
final class ProfileInfoHeaderCollectionReusableView: UICollectionReusableView {
enum Output {
case postButtonDidTap
case follwerButtonDidTap
case followingButtonDidTap
case editButtonDidTap
}
static let identifier = "ProfileInfoHeaderCollectionReusableView"
private let output: PassthroughSubject<Output, Never> = .init()
private let profilePhotoImageView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .systemRed
imageView.layer.masksToBounds = true
return imageView
}()
private let postButton: UIButton = {
let button = UIButton()
button.setTitle("Posts", for: .normal)
button.setTitleColor(.label, for: .normal)
button.backgroundColor = .secondarySystemBackground
return button
}()
private let follwerButton: UIButton = {
let button = UIButton()
button.setTitle("Follwers", for: .normal)
button.setTitleColor(.label, for: .normal)
button.backgroundColor = .secondarySystemBackground
return button
}()
private let followingButton: UIButton = {
let button = UIButton()
button.setTitle("Following", for: .normal)
button.setTitleColor(.label, for: .normal)
button.backgroundColor = .secondarySystemBackground
return button
}()
private let editProfileButton: UIButton = {
let button = UIButton()
button.setTitle("Edit Profile", for: .normal)
button.setTitleColor(.label, for: .normal)
button.backgroundColor = .secondarySystemBackground
return button
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.numberOfLines = 1
label.text = "Test User"
return label
}()
private let bioLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.numberOfLines = 0
label.text = "This is the first account!"
return label
}()
private var cancellabels = Set<AnyCancellable>()
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
bind()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func transform() -> AnyPublisher<Output, Never> {
return output.eraseToAnyPublisher()
}
override func layoutSubviews() {
super.layoutSubviews()
let profilePhotoSize = width / 4
profilePhotoImageView.frame = CGRect(x: 5, y: 5, width: profilePhotoSize, height: profilePhotoSize).integral
profilePhotoImageView.layer.cornerRadius = profilePhotoSize / 2.0
let buttonHeight = profilePhotoSize / 2
let countButtonWidth = (width - 10 - profilePhotoSize) / 3
postButton.frame = CGRect(x: profilePhotoImageView.right, y: 5, width: countButtonWidth, height: buttonHeight).integral
follwerButton.frame = CGRect(x: postButton.right, y: 5, width: countButtonWidth, height: buttonHeight).integral
followingButton.frame = CGRect(x: follwerButton.right, y: 5, width: countButtonWidth, height: buttonHeight).integral
editProfileButton.frame = CGRect(x: profilePhotoImageView.right, y: 5 + buttonHeight, width: countButtonWidth * 3, height: buttonHeight).integral
nameLabel.frame = CGRect(x: 5, y: 5 + profilePhotoImageView.bottom, width: width - 10, height: 50).integral
let bioLabelSize = bioLabel.sizeThatFits(frame.size)
bioLabel.frame = CGRect(x: 5, y: 5 + nameLabel.bottom, width: width - 10, height: bioLabelSize.height).integral
}
private func setUI() {
backgroundColor = .systemBackground
clipsToBounds = true
addSubview(profilePhotoImageView)
addSubview(postButton)
addSubview(follwerButton)
addSubview(followingButton)
addSubview(editProfileButton)
addSubview(nameLabel)
addSubview(bioLabel)
}
private func bind() {
follwerButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.follwerButtonDidTap)
}
.store(in: &cancellabels)
followingButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.followingButtonDidTap)
}
.store(in: &cancellabels)
editProfileButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.editButtonDidTap)
}
.store(in: &cancellabels)
postButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.postButtonDidTap)
}
.store(in: &cancellabels)
}
}