Build Instagram App: Part 12 (Swift 5) - 2020 - Xcode 11 - iOS Development
private func bind() {
followButton
.tapPublisher
.sink { [weak self] _ in
if let model = self?.model {
var state = FollowState.following
switch model.type {
case .follow(state: let currentState):
if state == currentState {
state = .not_following
}
self?.toggleButton(type: state)
case .like(post: _): break
}
let newModel = UserNotificationModel(type: .follow(state: state), text: model.text, user: model.user)
self?.output.send(.followButtonDidTap(model: newModel))
self?.model = newModel
}
}
.store(in: &cancellables)
}
private func bind(in output: AnyPublisher<NotificationFollowEventTableViewCell.Output, Never>) {
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .followButtonDidTap(model: let model):
var currentModel = self.viewModel.notificationModel.value
if let modelIndex = currentModel.firstIndex(where: {$0.user.email == model.user.email }) {
currentModel[modelIndex] = model
self.viewModel.notificationModel.send(currentModel)
}
}
}
.store(in: &cancellables)
}
import Foundation
import Combine
class NotificationViewModel {
let notificationModel: CurrentValueSubject<[UserNotificationModel], Never> = .init([])
private var cancellabels = Set<AnyCancellable>()
init() {
addSubscription()
}
private func addSubscription() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.addMockData()
}
}
private func addMockData() {
let counts = UserCountModel(follwers: 100, following: 100, posts: 100)
let photoURL = URL(string: "https://i.stack.imgur.com/GsDIl.jpg")!
var data = [UserNotificationModel]()
for x in 0..<100 {
let user = UserModel(email: "email" + "\(x)", bio: "bio", firstName: "firstName", lastName: "lastName", birthDate: Date(), counts: counts, gender: .male, joinDate: Date(), profilePhoto: photoURL)
if x % 2 == 0 {
let state = x < 50 ? FollowState.following : FollowState.not_following
let noti = UserNotificationModel(type: .follow(state: state), text: "noti \(x)", user: user)
data.append(noti)
} else {
let post = UserPostModel(identifier: "post \(x)", postType: .photo, postURL: photoURL, thumnailImage: photoURL, caption: "Caption", comments: [], likeCount: [], createdDate: Date(), taggedUers: [user])
let noti = UserNotificationModel(type: .like(post: post), text: "noti \(x)", user: user)
data.append(noti)
}
}
notificationModel.send(data)
}
}
import UIKit
import Combine
final class NotificationViewController: UIViewController {
private let tableView: UITableView = {
let tableView = UITableView()
tableView.isHidden = true
tableView.register(NotificationLikeTableViewCell.self, forCellReuseIdentifier: NotificationLikeTableViewCell.identifier)
tableView.register(NotificationFollowEventTableViewCell.self, forCellReuseIdentifier: NotificationFollowEventTableViewCell.identifier)
return tableView
}()
private lazy var noNotificationView = NoNotificationView()
private let spinner: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView(style: .large)
spinner.hidesWhenStopped = true
spinner.tintColor = .label
return spinner
}()
private var cancellables = Set<AnyCancellable>()
private let viewModel = NotificationViewModel()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
noNotificationView.frame = CGRect(x: 0, y: 0, width: view.width / 2, height: view.width / 4)
noNotificationView.center = view.center
spinner.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
spinner.center = view.center
}
private func setUI() {
view.addSubview(spinner)
spinner.startAnimating()
view.backgroundColor = .systemBackground
view.addSubview(tableView)
view.addSubview(noNotificationView)
noNotificationView.isHidden = true
navigationItem.title = "Notifications"
tableView.delegate = self
tableView.dataSource = self
}
private func bind() {
viewModel
.notificationModel
.receive(on: DispatchQueue.main)
.sink { [weak self] model in
self?.spinner.stopAnimating()
if !model.isEmpty {
self?.tableView.isHidden = false
self?.noNotificationView.isHidden = true
self?.tableView.reloadData()
print("Notifcation View Reload")
} else {
self?.noNotificationView.isHidden = false
self?.tableView.isHidden = true
}
}
.store(in: &cancellables)
}
}
extension NotificationViewController: UITableViewDelegate {
}
extension NotificationViewController {
private func bind(in output: AnyPublisher<NotificationFollowEventTableViewCell.Output, Never>) {
output
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .followButtonDidTap(model: let model):
var currentModel = self.viewModel.notificationModel.value
if let modelIndex = currentModel.firstIndex(where: {$0.user.email == model.user.email }) {
currentModel[modelIndex] = model
self.viewModel.notificationModel.send(currentModel)
}
}
}
.store(in: &cancellables)
}
private func bind(in output: AnyPublisher<NotificationLikeTableViewCell.Output, Never>) {
}
}
extension NotificationViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = viewModel.notificationModel.value[indexPath.row]
switch model.type {
case .like(post: _):
guard let cell = tableView.dequeueReusableCell(withIdentifier: NotificationLikeTableViewCell.identifier, for: indexPath) as? NotificationLikeTableViewCell else { return UITableViewCell() }
cell.configure(with: model)
let output = cell.transform()
bind(in: output.eraseToAnyPublisher())
return cell
case .follow:
guard let cell = tableView.dequeueReusableCell(withIdentifier: NotificationFollowEventTableViewCell.identifier, for: indexPath) as? NotificationFollowEventTableViewCell else { return UITableViewCell() }
cell.configure(with: model)
let output = cell.transform()
bind(in: output.eraseToAnyPublisher())
return cell
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.notificationModel.value.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 52
}
}
Diffable Data Source
를 사용하거나 비동기 처리를 통해 다운로드받은 이미지 데이터 등을 캐시화하여 핸들링 가능import UIKit
import Combine
class NotificationFollowEventTableViewCell: UITableViewCell {
enum Output {
case followButtonDidTap(model: UserNotificationModel)
}
static let identifier = "NotificationFollowEventTableViewCell"
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private let label: UILabel = {
let label = UILabel()
label.textColor = .label
label.numberOfLines = 0
return label
}()
private let followButton: UIButton = {
let button = UIButton()
button.layer.cornerRadius = 4
button.layer.masksToBounds = true
return button
}()
private var output: PassthroughSubject<Output, Never> = .init()
private var model: UserNotificationModel?
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()
profileImageView.frame = CGRect(x: 3, y: 3, width: contentView.height - 6, height: contentView.height - 6)
profileImageView.layer.cornerRadius = profileImageView.height / 2
let size:CGFloat = 100
let buttonHeight:CGFloat = 40
followButton.frame = CGRect(x: contentView.width - 5 - size, y: (contentView.height - buttonHeight) / 2, width: size, height: buttonHeight)
label.frame = CGRect(x: profileImageView.right + 5, y: 0, width: contentView.width - size - profileImageView.width - 16, height: contentView.height)
}
override func prepareForReuse() {
super.prepareForReuse()
profileImageView.image = nil
followButton.layer.borderWidth = 0
followButton.setTitle(nil, for: .normal)
followButton.backgroundColor = nil
label.text = nil
output.send(completion: .finished)
}
private func setUI() {
contentView.clipsToBounds = true
contentView.addSubview(profileImageView)
contentView.addSubview(followButton)
contentView.addSubview(label)
}
private func toggleButton(type: FollowState) {
if type == .following {
followButton.setTitle("Unfollow", for: .normal)
followButton.setTitleColor(.label, for: .normal)
followButton.backgroundColor = .systemBackground
followButton.layer.borderWidth = 1
followButton.layer.borderColor = UIColor.secondaryLabel.cgColor
} else {
followButton.setTitle("Follow", for: .normal)
followButton.setTitleColor(.label, for: .normal)
followButton.backgroundColor = .systemBlue
followButton.layer.borderWidth = 0
}
}
func configure(with model: UserNotificationModel) {
output = .init()
self.model = model
switch model.type {
case .follow(let state): toggleButton(type: state)
case .like(post: let post): break
}
label.text = model.text
var imageSubscription: AnyCancellable?
imageSubscription = NetworkingManager
.download(with: model.user.profilePhoto)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] data in
self?.profileImageView.image = UIImage(data: data)
imageSubscription?.cancel()
})
}
func transform() -> AnyPublisher<Output, Never> {
return output.eraseToAnyPublisher()
}
private func bind() {
followButton
.tapPublisher
.sink { [weak self] _ in
if let model = self?.model {
var state = FollowState.following
switch model.type {
case .follow(state: let currentState):
if state == currentState {
state = .not_following
}
self?.toggleButton(type: state)
case .like(post: _): break
}
let newModel = UserNotificationModel(type: .follow(state: state), text: model.text, user: model.user)
self?.output.send(.followButtonDidTap(model: newModel))
self?.model = newModel
}
}
.store(in: &cancellables)
}
}
import UIKit
import Combine
class NotificationLikeTableViewCell: UITableViewCell {
enum Output {
case postButtonDidTap(model: UserNotificationModel)
}
static let identifier = "NotificationLikeTableViewCell"
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")
}
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.backgroundColor = .tertiarySystemBackground
return imageView
}()
private let label: UILabel = {
let label = UILabel()
label.textColor = .label
label.numberOfLines = 0
label.text = "@Noah liked your photo"
return label
}()
private let postButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(named: "test"), for: .normal)
return button
}()
private var output: PassthroughSubject<Output, Never> = .init()
private var model: UserNotificationModel?
private var cancellables = Set<AnyCancellable>()
override func layoutSubviews() {
super.layoutSubviews()
profileImageView.frame = CGRect(x: 3, y: 3, width: contentView.height - 6, height: contentView.height - 6)
profileImageView.layer.cornerRadius = profileImageView.height / 2
let size = contentView.height - 4
postButton.frame = CGRect(x: contentView.width - size - 5, y: 2, width: size, height: size)
label.frame = CGRect(x: profileImageView.right + 5, y: 0, width: contentView.width - size - profileImageView.width - 16, height: contentView.height)
}
override func prepareForReuse() {
super.prepareForReuse()
profileImageView.image = nil
postButton.layer.borderWidth = 0
postButton.setTitle(nil, for: .normal)
postButton.setBackgroundImage(nil, for: .normal)
label.text = nil
output.send(completion: .finished)
}
private func setUI() {
contentView.clipsToBounds = true
contentView.addSubview(profileImageView)
contentView.addSubview(postButton)
contentView.addSubview(label)
}
private func bind() {
postButton
.tapPublisher
.sink { [weak self] _ in
if let model = self?.model {
self?.output.send(.postButtonDidTap(model: model))
}
}
.store(in: &cancellables)
}
func configure(with model: UserNotificationModel) {
output = .init()
self.model = model
switch model.type {
case .follow: break
case .like(post: let post):
var thumbnailSubscription: AnyCancellable?
thumbnailSubscription = NetworkingManager
.download(with: post.thumnailImage)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] data in
self?.postButton.setBackgroundImage(UIImage(data: data), for: .normal)
thumbnailSubscription?.cancel()
})
}
label.text = model.text
var imageSubscription: AnyCancellable?
imageSubscription = NetworkingManager
.download(with: model.user.profilePhoto)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] data in
self?.profileImageView.image = UIImage(data: data)
imageSubscription?.cancel()
})
}
func transform() -> AnyPublisher<Output, Never> {
return output.eraseToAnyPublisher()
}
}