Build Instagram App: Part 15 (Swift 5) - 2020 - Xcode 11 - iOS Development
private func bind(in output: AnyPublisher<IGFeedPostHeaderTableViewCell.Output, Never>) {
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .didTapMoreButton: self?.showActionSheet()
}
}
.store(in: &cancellables)
}
func configure(with model: UserModel) {
output = .init()
userNameLabel.text = model.firstName + model.lastName
var imageSubscription: AnyCancellable?
imageSubscription = NetworkingManager
.download(with: model.profilePhoto)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
self?.profilePhotoImageView.image = UIImage(data: data)
imageSubscription?.cancel()
})
}
private func bind() {
moreButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.didTapMoreButton)
}
override func prepareForReuse() {
super.prepareForReuse()
profilePhotoImageView.image = nil
userNameLabel.text = nil
output.send(completion: .finished)
}
configure
부분에서 output
을 새롭게 이니셜라이즈하기 때문에 동작 보장mport UIKit
import Combine
class HomeViewController: UIViewController {
private let input: PassthroughSubject<HomeViewModel.Input, Never> = .init()
private let viewModel = HomeViewModel()
private var cancellables = Set<AnyCancellable>()
private let tableView: UITableView = {
let tableView = UITableView()
tableView.register(IGFeedPostTableViewCell.self, forCellReuseIdentifier: IGFeedPostTableViewCell.identifier)
tableView.register(IGFeedPostHeaderTableViewCell.self, forCellReuseIdentifier: IGFeedPostHeaderTableViewCell.identifier)
tableView.register(IGFeedPostGeneralTableViewCell.self, forCellReuseIdentifier: IGFeedPostGeneralTableViewCell.identifier)
tableView.register(IGFeedPostActionsTableViewCell.self, forCellReuseIdentifier: IGFeedPostActionsTableViewCell.identifier)
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
input.send(.isAlreadyLogin)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
}
private func bind() {
let output = viewModel.transform(input: input.eraseToAnyPublisher())
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .loginOutput(result: let result):
if !result {
self?.handleNotLogined()
}
}
}
.store(in: &cancellables)
}
private func handleNotLogined() {
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .fullScreen
present(loginVC, animated: true)
}
}
extension HomeViewController: UITableViewDelegate {
}
extension HomeViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let x = section
let model: HomeFeedRenderViewModel
if x == 0 {
model = viewModel.feedRenderModels[0]
} else {
let position = x % 4 == 0 ? x / 4 : ((x - (x % 4)) / 4)
model = viewModel.feedRenderModels[position]
}
let subSection = x % 4
switch subSection {
case 0:
//header
return 1
case 1:
// post
return 1
case 2:
return 1
// actions
case 3:
let commentsModel = model.comments
switch commentsModel.renderType {
case .comments(comments: let comments):
return comments.count > 2 ? 2 : comments.count
default: return 0
}
// comments
default: return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let x = indexPath.section
let model: HomeFeedRenderViewModel
if x == 0 {
model = viewModel.feedRenderModels[0]
} else {
let position = x % 4 == 0 ? x / 4 : ((x - (x % 4)) / 4)
model = viewModel.feedRenderModels[position]
}
let subSection = x % 4
switch subSection {
case 0:
//header
let headerModel = model.header
switch headerModel.renderType {
case .header(provider: let user):
guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostHeaderTableViewCell.identifier, for: indexPath) as? IGFeedPostHeaderTableViewCell else { return UITableViewCell() }
cell.configure(with: user)
let output = cell.transform()
bind(in: output)
return cell
default: return UITableViewCell()
}
case 1:
// post
let postModel = model.post
switch postModel.renderType {
case .primaryContent(providier: let post):
guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostTableViewCell.identifier, for: indexPath) as? IGFeedPostTableViewCell else { return UITableViewCell() }
cell.configure(with: post)
return cell
default: return UITableViewCell()
}
case 2:
let actionModel = model.actions
switch actionModel.renderType {
case .actions(provider: let action):
guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostActionsTableViewCell.identifier, for: indexPath) as? IGFeedPostActionsTableViewCell else { return UITableViewCell() }
let output = cell.transform()
bind(in: output)
return cell
default: return UITableViewCell()
}
// actions
case 3:
let commentModel = model.comments
// comments
switch commentModel.renderType {
case .comments(comments: let comments):
guard let cell = tableView.dequeueReusableCell(withIdentifier: IGFeedPostGeneralTableViewCell.identifier, for: indexPath) as? IGFeedPostGeneralTableViewCell else { return UITableViewCell() }
return cell
default: return UITableViewCell()
}
default: return UITableViewCell()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.feedRenderModels.count * 4
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let subSection = indexPath.section % 4
switch subSection {
case 0: return 70
case 1: return tableView.width
case 2: return 60
case 3: return 50
default: return .zero
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
let subSection = section % 4
return subSection == 3 ? 70 : 0
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}
}
extension HomeViewController {
private func bind(in output: AnyPublisher<IGFeedPostHeaderTableViewCell.Output, Never>) {
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .didTapMoreButton: self?.showActionSheet()
}
}
.store(in: &cancellables)
}
private func bind(in output: AnyPublisher<IGFeedPostActionsTableViewCell.Output, Never>) {
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
switch result {
case .likeButtonDidTap:
break
case .commentButtonDidTap:
break
case .sendButtonDidTap:
break
}
}
.store(in: &cancellables)
}
private func showActionSheet() {
let actionSheet = UIAlertController(title: "Post Options", message: nil, preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
actionSheet.addAction(UIAlertAction(title: "Report Post", style: .destructive, handler: { [weak self] _ in
self?.reportPost()
}))
present(actionSheet, animated: true)
}
private func reportPost() {
}
}
import UIKit
import Combine
import AVFoundation
class IGFeedPostTableViewCell: UITableViewCell {
static let identifier = "IGFeedPostTableViewCell"
private let postImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = nil
return imageView
}()
private var player: AVPlayer?
private var playerLayer = AVPlayerLayer()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
postImageView.frame = contentView.bounds
playerLayer.frame = contentView.bounds
}
override func prepareForReuse() {
super.prepareForReuse()
postImageView.image = nil
}
private func setUI() {
contentView.layer.addSublayer(playerLayer)
contentView.addSubview(postImageView)
}
func configure(with model: UserPostModel) {
switch model.postType {
case .photo:
var photoSubscription: AnyCancellable?
photoSubscription = NetworkingManager
.download(with: model.postURL)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
self?.postImageView.image = UIImage(data: data)
photoSubscription?.cancel()
})
case .video:
player = AVPlayer(url: model.postURL)
playerLayer.player = player
playerLayer.player?.volume = 0
playerLayer.player?.play()
// load and play video
}
}
}
import UIKit
import Combine
class IGFeedPostHeaderTableViewCell: UITableViewCell {
enum Output {
case didTapMoreButton
}
static let identifier = "IGFeedPostHeaderTableViewCell"
private let profilePhotoImageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private let userNameLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.numberOfLines = 1
label.font = .systemFont(ofSize: 18, weight: .medium)
return label
}()
private let moreButton: UIButton = {
let button = UIButton()
button.tintColor = .label
button.setImage(UIImage(systemName: "ellipsis"), for: .normal)
return button
}()
private var output: PassthroughSubject<Output, Never> = .init()
private var cancellables = Set<AnyCancellable>()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUI()
bind()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let size = contentView.height - 4
profilePhotoImageView.frame = CGRect(x: 2, y: 2, width: size, height: size)
profilePhotoImageView.layer.cornerRadius = size / 2
moreButton.frame = CGRect(x: contentView.width - size - 2, y: 2, width: size, height: size)
userNameLabel.frame = CGRect(x: profilePhotoImageView.right + 10, y: 2, width: contentView.width - (size * 2) - 15, height: contentView.height - 4)
}
private func setUI() {
contentView.addSubview(profilePhotoImageView)
contentView.addSubview(userNameLabel)
contentView.addSubview(moreButton)
}
func configure(with model: UserModel) {
output = .init()
userNameLabel.text = model.firstName + model.lastName
var imageSubscription: AnyCancellable?
imageSubscription = NetworkingManager
.download(with: model.profilePhoto)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] data in
self?.profilePhotoImageView.image = UIImage(data: data)
imageSubscription?.cancel()
})
}
func transform() -> AnyPublisher<Output, Never> {
return output.eraseToAnyPublisher()
}
private func bind() {
moreButton
.tapPublisher
.sink { [weak self] _ in
self?.output.send(.didTapMoreButton)
}
.store(in: &cancellables)
}
override func prepareForReuse() {
super.prepareForReuse()
profilePhotoImageView.image = nil
userNameLabel.text = nil
output.send(completion: .finished)
}
}