스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

63. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #5

seongpil Heo 2025. 6. 12. 23:16

  👨🏻‍💻  오늘의 작업 

[ 1. 상세화면 TableView UI PR ]

 

[Feat] #1 - 상세화면 TableView UI 구현 by heopill · Pull Request #24 · uddt-ds/EatsOkay

📌 관련 이슈 closed: #1 📌 변경 사항 및 이유 TableView rxDataSource를 사용해서 구현 Popup Button 구현 📌 ScreenShot 📌 PR Point 매장 카운드 라벨과 정렬 버튼은 동일 선상 정렬버튼의 위치를 비율로 설정

github.com

 

[ 2. ReactorKit 학습 ]

 

GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

A library for reactive and unidirectional Swift applications - ReactorKit/ReactorKit

github.com



 

[ 3. Reactor 작성 ]

DetailReactor.swift
import Foundation
import ReactorKit
import RxSwift

class DetailReactor: Reactor {
    var initialState: State
    var seletedKeywords: [String] // home에서 전달받는 검색 키워드
    
    init(seletedKeywords: [String]) {
        self.seletedKeywords = seletedKeywords
        self.initialState = State()
    }
    
    enum Action {
        case viewDidLoad // 뷰가 DidLoad 되었을 때
        case tableViewItemTapped // 테이블 뷰 셀을 클릭했을 때
        case sortButtonTapped // 정렬 버튼을 클릭했을 때
        case webViewDidDismiss // 웹뷰가 닫혔을 때
    }
    
    enum Mutation {
        case setStore([StoreSection])
        case presentWebView // 모달
        case sortingData // 데이터 정렬
        case dismissWebView // 웹뷰가 닫혔을 때
    }
    
    struct State {
        var storeInfo = [StoreSection]()
        var shouldPresentWebView: Bool = false // 초기 웹뷰 여부 false
    }
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .viewDidLoad:
            // 네트워크 통신 하고
            // 일단은 목데이터
            let StoreInfo = [
                StoreSection(items: [StoreInfo(
                    displayName: "장충동 족발",
                    formattedAddress: "서울시 강남구 테헤란로 123",
                    latitude: 37.498,
                    longitude: 127.027,
                    rating: 4.5,
                    googleMapsURI: "maps://store1",
                    userRatingCount: 120,
                    photosNames: ["store1"]
                ),StoreInfo(
                    displayName: "샤브올데이",
                    formattedAddress: "서울시 서초구 테헤란로 123",
                    latitude: 37.498,
                    longitude: 127.027,
                    rating: 4.9,
                    googleMapsURI: "maps://store1",
                    userRatingCount: 1245,
                    photosNames: ["store1"]
                ),
                ])
            ]
            return Observable.just(.setStore(StoreInfo)) // 데이터 받아서 넣기
        case .tableViewItemTapped:
            return Observable.just(.presentWebView) // 웹뷰 띄우기
        case .sortButtonTapped:
            return Observable.just(.sortingData) // storeInfo 정렬
        case .webViewDidDismiss:
            return Observable.just(.dismissWebView) // viewDidmiss
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setStore(let storeInfo):
            newState.storeInfo = storeInfo
        case .presentWebView:
            newState.shouldPresentWebView = true
        case .sortingData:
            break // 아직 미구현
        case .dismissWebView:
            newState.shouldPresentWebView = false
        }
        return newState
    }
}

 

DetailViewController.swift
import ReactorKit // Reactor 사용을 위해 추가
import SafariServices // SafariWebView 사용을 위해 추가

class DetailViewController: UIViewController, View {
    typealias Reactor = DetailReactor
    
    var disposeBag = DisposeBag()
    let reactor = DetailReactor(seletedKeywords: ["치킨", "족발"])
    
    // 뷰 구현 코드 생략
    
    // MARK: - viewDidLoad -
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        bind(reactor: reactor) // 추가
    }
    
    // MARK: - Reactor bind -
    func bind(reactor: DetailReactor) {
        bindAction(reactor: reactor) // Action과 State로 분리
        bindState(reactor: reactor)	// Action과 State로 분리
    }
    
    func bindAction(reactor: DetailReactor) {
        // viewDidLoad 될 때 Action
        reactor.action.onNext(.viewDidLoad) // 주로 just 사용
        
        // 테이블 뷰 cell 클릭시 Action
        tableView.rx.itemSelected
            .map { _ in Reactor.Action.tableViewItemTapped }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // 정렬 버튼 클릭시 Action
        sortButton.rx.tap
            .map { Reactor.Action.sortButtonTapped }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
    }
    
    func bindState(reactor: DetailReactor) {
    	// rxDataSource 사용
        let dataSource = RxTableViewSectionedAnimatedDataSource<StoreSection>(
            configureCell: { dataSource, tableView, indexPath, item in
                let cell = tableView.dequeueReusableCell(withIdentifier: DetailTableViewCell.identifier, for: indexPath) as! DetailTableViewCell
                cell.configureView(with: item)
                return cell
            }
        )
        
        // 테이블 뷰 State 바인딩
        reactor.state
            .map { $0.storeInfo }
            .asDriver(onErrorDriveWith: .empty())
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.shouldPresentWebView }
            .distinctUntilChanged()
            .filter { $0 }
            .subscribe(onNext: { [weak self] _ in
                guard let self = self else { return }
                let url = URL(string: "https://github.com/heopill")!
                let safariVC = SFSafariViewController(url: url)
                safariVC.delegate = self
                self.present(safariVC, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
    }
    
}

// Safari webView didFinish 델리게이트
extension DetailViewController: SFSafariViewControllerDelegate {
    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        reactor.action.onNext(.webViewDidDismiss) // webViewDidDismiss false로 초기화
    }
}

  🎯  TroubleShooting

[ 1. modal을 .popover로 dismiss 할 때 safariViewControllerDidFinish 메서드에서 잡아내지 못함 ]

 

해결 방법
reactor.state
    .map { $0.shouldPresentWebView }
    .distinctUntilChanged()
    .filter { $0 }
    .subscribe(onNext: { [weak self] _ in
        guard let self = self else { return }
        let url = URL(string: "https://github.com/heopill")!
        let safariVC = SFSafariViewController(url: url)
        safariVC.modalPresentationStyle = .popover
        safariVC.delegate = self
        safariVC.presentationController?.delegate = self // 추가
        self.present(safariVC, animated: true, completion: nil)
    })
    .disposed(by: disposeBag)

 

extension DetailViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        reactor.action.onNext(.webViewDidDismiss)
    }
}

 

presentationControllerDidDismiss 메서드에 reactor.action.onNext(.webViewDidDismiss)를 작성해서
Reactor에서 false로 처리할 수 있게 해준다.