👨🏻💻 오늘의 작업
[ 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로 처리할 수 있게 해준다.
'스파르타 코딩 클럽 - iOS 스타터 6기 > 본 캠프' 카테고리의 다른 글
65. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #7 (1) | 2025.06.16 |
---|---|
64. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #6 (1) | 2025.06.13 |
62. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #4 (0) | 2025.06.11 |
61. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #3 (0) | 2025.06.10 |
59. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #2 (0) | 2025.06.06 |