[UIKit] InstagramClone: SettingsView 2

Junyoung Park·2022년 11월 6일
0

UIKit

목록 보기
83/142
post-thumbnail

Build Instagram App: Part 6 (Swift 5) - 2020 - Xcode 11 - iOS Development

InstagramClone: SettingsView 2

구현 목표

  • 세팅 뷰 UI 보완

구현 태스크

  • 섹션의 로우 별 함수 인풋/아웃풋 핸들링

핵심 코드

private func bind() {
        let output = viewModel.transform(input: input.eraseToAnyPublisher())
        output
            .receive(on: DispatchQueue.main)
            .sink { [weak self] result in
                switch result {
                case .showEditProfileView:
                    self?.showEditProfileView()
                case .isLogoutDidSucceed(result: let isSucceeded):
                    self?.handleLogout(result: isSucceeded)
                case .openURL(url: let url):
                    self?.handleURL(with: url)
                }
            }
            .store(in: &cancellables)
    }
  • 뷰 컨트롤러는 뷰 모델에서 sink를 통해 내려받은 아웃풋의 종류에 따라 어떤 인터렉션을 보여줄지 결정
private func handleURL(with url: URL) {
        let vc = SFSafariViewController(url: url)
        present(vc, animated: true)
    }
  • 동일한 액션, URL만 다르다면 같은 함수 사용
private func handleURLString(with urlString: String) {
        guard let url = URL(string: urlString) else { return }
        output.send(.openURL(url: url))
    }
  • 뷰 모델 또한 URL 문자열을 제외하고는 동일한 로직

소스 코드

import UIKit
import Combine
import SafariServices

class SettingsViewController: UIViewController {
    private let tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .grouped)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableViewCell")
        return tableView
    }()
    private let input: PassthroughSubject<SettingsViewModel.Input, Never> = .init()
    private let viewModel = SettingsViewModel()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        setUI()
        bind()
    }
    
    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 .showEditProfileView:
                    self?.showEditProfileView()
                case .isLogoutDidSucceed(result: let isSucceeded):
                    self?.handleLogout(result: isSucceeded)
                case .openURL(url: let url):
                    self?.handleURL(with: url)
                }
            }
            .store(in: &cancellables)
    }
    
    private func showEditProfileView() {
        let vc = EditProfileViewController()
        let navVC = UINavigationController(rootViewController: vc)
        present(navVC, animated: true)
    }
    
    private func handleURL(with url: URL) {
        let vc = SFSafariViewController(url: url)
        present(vc, animated: true)
    }
    
    private func handleLogout(result: Bool) {
        if result {
            let loginVC = LoginViewController()
            loginVC.modalPresentationStyle = .fullScreen
            present(loginVC, animated: true) {
                self.navigationController?.popToRootViewController(animated: false)
                self.tabBarController?.selectedIndex = 0
            }
        }
    }
    
    private func logoutButtonDidTap() {
        let actionSheet = UIAlertController(title: "Log Out", message: "Are you sure you want to log out?", preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        actionSheet.addAction(UIAlertAction(title: "Log Out", style: .destructive, handler: { [weak self] _ in
            self?.input.send(.logoutDidTap)
        }))
        actionSheet.popoverPresentationController?.sourceView = tableView
        actionSheet.popoverPresentationController?.sourceRect = tableView.bounds
        present(actionSheet, animated: true)
    }
}

extension SettingsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let model = viewModel.settingsModel.value[indexPath.section][indexPath.row]
        switch model {
        case "Log Out": logoutButtonDidTap()
        case "Edit Profile": input.send(.editProfileDidTap)
        case "Invite Friends": input.send(.inviteFriendDidTap)
        case "Save Original Posts": input.send(.saveOriginalPostsDidTap)
        case "Terms of Service": input.send(.termsOfServiceDidTap)
        case "Privacy Policy": input.send(.privacyPolicyDidTap)
        case "Help / Feedback": input.send(.helpAndFeedbackDidTap)
        default: break
        }
    }
}

extension SettingsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
        let model = viewModel.settingsModel.value[indexPath.section][indexPath.row]
        cell.textLabel?.text = model
        cell.accessoryType = .disclosureIndicator
        return cell
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.settingsModel.value.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.settingsModel.value[section].count
    }
}

  • 프로필 조정은 새로운 모달 뷰, 정보 관련 셀은 사파리의 웹 뷰 컨트롤 담당
import Foundation
import Combine

class SettingsViewModel {
    enum Input {
        case logoutDidTap
        case editProfileDidTap
        case inviteFriendDidTap
        case saveOriginalPostsDidTap
        case termsOfServiceDidTap
        case privacyPolicyDidTap
        case helpAndFeedbackDidTap
    }
    enum Output {
        case openURL(url: URL)
        case isLogoutDidSucceed(result: Bool)
        case showEditProfileView
    }
    private let output: PassthroughSubject<Output, Never> = .init()
    let settingsModel: CurrentValueSubject<[[String]], Never> = .init([])
    private var cancellabels = Set<AnyCancellable>()
    private let authManager = AuthManager.shared
    
    init() {
        addSubscription()
    }
    
    private func addSubscription() {
        settingsModel.send([["Edit Profile", "Invite Friends", "Save Original Posts"], ["Terms of Service", "Privacy Policy", "Help / Feedback"], ["Log Out"]])
    }
    
    func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
        input
            .receive(on: DispatchQueue.global(qos: .default))
            .sink { [weak self] result in
                switch result {
                case .logoutDidTap: self?.handleLogout()
                case .editProfileDidTap:
                    self?.output.send(.showEditProfileView)
                case .inviteFriendDidTap:
                    break
                case .saveOriginalPostsDidTap:
                    break
                case .termsOfServiceDidTap:
                    self?.handleURLString(with: "https://help.instagram.com/581066165581870")
                case .privacyPolicyDidTap:
                    self?.handleURLString(with: "https://help.instagram.com/155833707900388")
                case .helpAndFeedbackDidTap:
                    self?.handleURLString(with: "https://help.instagram.com")
                }
            }
            .store(in: &cancellabels)
        return output.eraseToAnyPublisher()
    }
    
    private func handleURLString(with urlString: String) {
        guard let url = URL(string: urlString) else { return }
        output.send(.openURL(url: url))
    }
    
    private func handleLogout() {
        var logoutSubscription: AnyCancellable?
        logoutSubscription = authManager
            .logout()
            .sink { [weak self] completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                    self?.output.send(.isLogoutDidSucceed(result: false))
                    logoutSubscription?.cancel()
                case .finished: break
                }
            } receiveValue: { [weak self] isSucceeded in
                self?.output.send(.isLogoutDidSucceed(result: isSucceeded))
                logoutSubscription?.cancel()
            }
    }
}
  • 기존의 세팅 뷰 컨트롤러와 뷰 모델 간의 연결 로직 상동
  • 인풋에 대한 디테일 액션 함수 추가

구현 화면

profile
JUST DO IT

0개의 댓글