[UIKit] BlogClone: Profile

Junyoung Park·2022년 11월 23일
0

UIKit

목록 보기
100/142
post-thumbnail
post-custom-banner

Building Subscription Blogging App: Part 6 – Profiles (2021, Xcode 12, Swift 5) – iOS

BlogClone: Profile

구현 목표

  • 프로필 정보 UI 구현
  • 작성 블로그 포스트 테이블 뷰 UI 구현

구현 태스크

  • 테이블 뷰 헤더 커스텀 UI 구현
  • 이미지 선택 시 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)
            }
        }
    }
}
  • 이미지 선택 이후 해당 이미지를 곧바로 UI에 적용
  • 별도로 현 시점의 유저 데이터베이스에 해당 이미지를 업로드한 URL 주소를 등록
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 등) 사실 강의 내용 중 현 시점에서 가장 어려운 지점은 프레임 레이아웃인데, 같이 고민해보자.

profile
JUST DO IT
post-custom-banner

0개의 댓글