[UIKit] InstragramClone: SettingsView

Junyoung Park·2022년 11월 6일
0

UIKit

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

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

InstagramClone:

구현 목표

  • 세팅 뷰 및 기타 뷰 UI
  • 로그아웃 기능 구현

구현 태스크

  • 세팅 뷰 로그아웃 기능 제공
  • 네비게이션 연결 및 이동

핵심 코드

private func setNavigationBar() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "gear")?.withTintColor(UIColor.black, renderingMode: .alwaysOriginal), style: .done, target: self, action: #selector(settingButtonDidTap))
    }
    @objc private func settingButtonDidTap() {
        let vc = SettingsViewController()
        vc.title = "Settings"
        navigationController?.pushViewController(vc, animated: true)
    }
  • 설정 뷰를 네비게이션 스택으로 연결하는 프로필 뷰의 네비게이션 바 아이템
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)
    }
  • 테이블 뷰의 로그아웃 로우 셀을 클릭할 시 발생하는 액션 시트
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()
            }
    }
  • one-shot subscription을 통해 뷰 모델에서 가지고 있는 인증 매니저의 로그아웃 함수 호출
  • 비동기 결과에 따라 뷰 컨트롤러에게 특정한 아웃풋 리턴
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
            }
        }
    }
  • 뷰 모델이 던져주는 아웃풋이 해당 로그아웃 기능일 때 그 결과 result에 따라 풀 모달 형태로 기존의 로그인 뷰를 올릴지 결정
  • 현재 네비게이션 스택이 쌓여 있으므로 로그인 뷰를 올린 뒤 스택을 모두 제거하고 탭 바의 인덱스를 홈 뷰로 변경
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()
                }
            }
            .store(in: &cancellabels)
        return output.eraseToAnyPublisher()
    }
  • 뷰 모델과 뷰 컨트롤러 간의 인풋/아웃풋을 연결하기 위한 뷰 모델의 커스텀 함수

소스 코드

import Foundation
import Combine

class SettingsViewModel {
    enum Input {
        case logoutDidTap
    }
    enum Output {
        case isLogoutDidSucceed(result: Bool)
    }
    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([["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()
                }
            }
            .store(in: &cancellabels)
        return output.eraseToAnyPublisher()
    }
    
    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()
            }
    }
}
  • 현쟈 테이블 뷰의 특정한 섹션 및 로우 셀을 일반적인 문자열의 이차원 배열로 관리 중
  • 이넘 또는 구조체를 통해 보다 편리하게 구현할 예정
import UIKit
import Combine

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 .isLogoutDidSucceed(result: let isSucceeded):
                    self?.handleLogout(result: isSucceeded)
                }
            }
            .store(in: &cancellables)
    }
    
    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]
        if model == "Log Out" {
            logoutButtonDidTap()
        }
    }
}

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
        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
    }
}
  • 설정 뷰 내뷰의 테이블 뷰 셀 구성이 다이나믹하게 변경되지 않는 까닭에 Diffable Data Source를 통해 구현하지 않아도 된다는 판단

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글