Building Subscription Blogging App: Part 6 – Profiles (2021, Xcode 12, Swift 5) – iOS
PHPickerViewController
사용UITableViewDiffableDataSource
사용func bind(with tableView: UITableView, input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
dataSource = ProfileTableViewDataSource(tableView: tableView, cellProvider: { tableView, indexPath, model in
guard let cell = tableView.dequeueReusableCell(withIdentifier: ProfileTableViewCell.identifier, for: indexPath) as? ProfileTableViewCell else { return nil }
cell.configure(with: model)
return cell
})
let dataSourceInput = dataSource.transform()
dataSourceInput
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .didSwipeCell(let indexPath):
var currentPosts = self.posts.value
currentPosts.remove(at: indexPath.row)
self.posts.send(currentPosts)
}
}
.store(in: &cancellables)
posts
.sink { [weak self] items in
self?.applySnapshot(items: items)
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
private func applySnapshot(items: [PostModel]) {
snapshot.deleteAllItems()
snapshot.appendSections(ProfileTableViewSection.allCases)
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
import Foundation
import UIKit
import Combine
enum ProfileTableViewSection: Int, CaseIterable {
case first
}
class ProfileTableViewDataSource: UITableViewDiffableDataSource<ProfileTableViewSection, PostModel> {
private let input: PassthroughSubject<ProfileViewModel.Input, Never> = .init()
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
input.send(.didSwipeCell(indexPath))
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func transform() -> AnyPublisher<ProfileViewModel.Input, Never> {
return input.eraseToAnyPublisher()
}
}
editStyle
등 테이블 뷰 내 델리게이트 함수를 오버라이드해서 사용하기 위함private func bind() {
let _ = viewModel.bind(with: tableView, input: input.eraseToAnyPublisher())
viewModel.authManager
.currentUser
.combineLatest(viewModel.authManager.profileImage)
.receive(on: DispatchQueue.main)
.sink { [weak self] (user, image) in
guard
let user = user,
let image = image else { return }
self?.tableHeaderView.configure(email: user.email, image: image)
self?.title = user.userName
}
.store(in: &cancellables)
}
AuthManager
의 두 퍼블리셔를 한 번에 구독(combineLatest
사용), 테이블 뷰 헤더의 프로필 정보 UI 구성protocol ProfileHeaderViewDelegate: AnyObject {
func didTapProfileImageView()
}
private func setUI() {
...
isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapProfileImageView))
profileImageView.addGestureRecognizer(tapGesture)
}
@objc private func didTapProfileImageView() {
delegate?.didTapProfileImageView()
}
extension ProfileViewController: ProfileHeaderViewDelegate {
func didTapProfileImageView() {
let config = PHPickerConfiguration()
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
picker.modalPresentationStyle = .fullScreen
present(picker, animated: true)
}
}
extension ProfileViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard
let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
guard
let image = image as? UIImage,
error == nil else { return }
self?.viewModel.authManager.changeProfileImage(with: image)
}
}
}
}
func changeProfileImage(with image: UIImage) {
guard let uid = userSession.value?.uid else { return }
profileImage.send(image)
ImageUploader.uploadImage(image: image, folder: .profileImage) { [weak self] result in
switch result {
case .failure(let error): print(error.localizedDescription)
case .success(let urlString):
Firestore.firestore().collection("users")
.document(uid)
.updateData(["profileImageURL": urlString]) { [weak self] error in
if let error = error {
print(error.localizedDescription)
}
}
}
}
}
확실히 알게 된 개념, 스타일이 있으니 이 방식대로 강의 내용을 적용시켜보고자 한다. (컴바인, DiffableDataSource 등) 사실 강의 내용 중 현 시점에서 가장 어려운 지점은 프레임 레이아웃인데, 같이 고민해보자.