import RIBs
import RxSwift
import RxRelay
import ReactorKit
protocol FAQsRouting: ViewableRouting {
func routeToFAQ(_ faq: FAQ)
func detachFAQ()
}
protocol FAQsPresentable: Presentable {
var reactor: FAQsReactor? { get set }
func bind(reactor: FAQsReactor)
}
protocol FAQsReactor: class {
var state: Observable<FAQsInteractor.State> { get }
var action: ActionSubject<FAQsInteractor.Action> { get }
var currentState: FAQsInteractor.State { get }
}
protocol FAQsListener: class {
func faqsViewDidDisappear()
}
extension FAQsInteractor {
func faqViewDidDisappear() {
self.router?.detachFAQ()
}
}
final class FAQsInteractor
: PresentableInteractor<FAQsPresentable>
, FAQsInteractable
, Reactor
, FAQsReactor {
typealias NextID = String
enum Action {
case refresh
case viewDidDisappear
case select(IndexPath)
}
enum Mutation {
case setRefreshing(Bool)
case setFAQs([FAQ])
case updateFAQ(FAQ, Int)
case updateSections
}
struct State {
var isRefreshing: Bool = false
var faqs: [FAQ] = []
var sections: [FAQsViewSection] = [FAQsViewSection(identity: .faqs, items: [])]
}
let initialState: State = State()
weak var router: FAQsRouting?
weak var listener: FAQsListener?
private let faqService: FAQServiceProtocol
init(presenter: FAQsPresentable, faqService: FAQServiceProtocol) {
defer { _ = self.state }
self.faqService = faqService
super.init(presenter: presenter)
self.presenter.reactor = self
}
override func didBecomeActive() {
super.didBecomeActive()
self.presenter.bind(reactor: self)
}
override func willResignActive() {
super.willResignActive()
}
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, self.faqServiceMutation())
}
private func faqServiceMutation() -> Observable<Mutation> {
return self.faqService.event.flatMap { [weak self] event -> Observable<Mutation> in
guard let self = self else { return .empty() }
switch event {
case let .viewed(faq):
guard let index = self.index(faqId: faq.id, from: self.currentState) else { return .empty() }
return Observable.concat([
Observable.just(.updateFAQ(faq, index)),
Observable.just(.updateSections)
])
}
}
}
private func index(faqId: String, from state: State) -> Int? {
let index = state.faqs.firstIndex { faq in faq.id == faqId }
if let index = index {
return index
} else {
return nil
}
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .select(indexPath):
let faq = self.currentState.faqs[indexPath.item]
self.router?.routeToFAQ(faq)
return Observable.empty()
case .viewDidDisappear:
self.listener?.faqsViewDidDisappear()
return Observable.empty()
case .refresh:
guard !self.currentState.isRefreshing else { return .empty() }
return Observable.concat([
Observable.just(.setRefreshing(true)),
self.refreshMutation(),
Observable.just(.updateSections),
Observable.just(.setRefreshing(false))
])
}
}
private func refreshMutation() -> Observable<Mutation> {
return self.faqService.faqs()
.map(Mutation.setFAQs)
.asObservable()
.catchError { _ in .empty() }
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case let .setRefreshing(isRefreshing):
newState.isRefreshing = isRefreshing
case let .setFAQs(faqs):
newState.faqs = faqs
case .updateSections:
newState.sections.removeAll()
defer { newState.sections.removeDuplicates() }
if !newState.faqs.isEmpty {
let items = newState.faqs.map(FAQsViewSection.Item.faq)
let section = FAQsViewSection(identity: .faqs, items: items)
newState.sections.append(section)
}
case let .updateFAQ(faq, index):
newState.faqs[index] = faq
}
return newState
}
}
import RIBs
import RxSwift
import AsyncDisplayKit
import RxCocoa
import RxCocoa_Texture
import RxDataSources_Texture
final class FAQsViewController: BaseViewController, FAQsPresentable {
// MARK: UI
private let refreshControl = RefreshControl()
private let collectionNode = ASCollectionNode(
collectionViewLayout: UICollectionViewFlowLayout()
).then {
$0.backgroundColor = .clear
$0.alwaysBounceVertical = true
$0.showsVerticalScrollIndicator = false
}
weak var reactor: FAQsReactor?
private lazy var dataSource = self.createDataSource()
private func createDataSource() -> RxASCollectionSectionedAnimatedDataSource<FAQsViewSection> {
return .init(
animationConfiguration: AnimationConfiguration(animated: false),
configureCellBlock: { dataSource, collectionNode, indexPath, sectionItem in
switch sectionItem {
case let .faq(faq):
return { FAQCellNode(faq: faq) }
}
}
)
}
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.titleView = NavigationBarTitleLabel(title: "FAQ")
self.collectionNode.view.addSubview(self.refreshControl)
}
func bind(reactor: FAQsReactor) {
self.bindRefresh(reactor: reactor)
self.bindDataSource(reactor: reactor)
self.bindDelegate(reactor: reactor)
self.bindDisappear(reactor: reactor)
self.bindSelectCell(reactor: reactor)
}
private func bindRefresh(reactor: FAQsReactor) {
self.rx.viewDidLoad
.map { .refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
self.refreshControl.rx.controlEvent(.valueChanged)
.map { .refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
reactor.state.map { $0.isRefreshing }
.distinctUntilChanged()
.bind(to: self.refreshControl.rx.isRefreshing)
.disposed(by: self.disposeBag)
}
func bindDataSource(reactor: FAQsReactor) {
reactor.state.map { $0.sections }
.distinctUntilChanged()
.bind(to: self.collectionNode.rx.items(dataSource: self.dataSource))
.disposed(by: self.disposeBag)
}
func bindDelegate(reactor: FAQsReactor) {
self.collectionNode.rx.setDelegate(self)
.disposed(by: self.disposeBag)
}
func bindSelectCell(reactor: FAQsReactor) {
self.collectionNode.rx.itemSelected
.map { .select($0) }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
}
private func bindDisappear(reactor: FAQsReactor) {
self.rx.viewDidDisappear
.map { [weak self] _ in self?.isMovingFromParent ?? false } // in view controller that is being popped
.filter { $0 }
.map { _ in .viewDidDisappear }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
}
// MARK: Layout
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
return ASWrapperLayoutSpec(layoutElement: self.collectionNode)
}
}
// MARK: FAQsViewControllable
extension FAQsViewController: FAQsViewControllable {
func push(viewController: ViewControllable?) {
if let viewController = viewController?.uiviewController {
self.navigationController?.pushViewController(viewController, animated: true)
}
}
}
extension FAQsViewController
: ASCollectionDelegateFlowLayout
, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .margins(top: 25)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 15
}
func collectionNode(_ collectionNode: ASCollectionNode, constrainedSizeForItemAt indexPath: IndexPath) -> ASSizeRange {
let width = UIScreen.main.bounds.width
return ASSizeRange(
min: CGSize(width: width, height: 0),
max: CGSize(width: width, height: CGFloat.infinity)
)
}
}
//#if canImport(SwiftUI) && DEBUG
//import SwiftUI
//
//struct FAQsViewController_Preview
// : UIViewControllerRepresentable
// , PreviewProvider {
//
// func makeUIViewController(context: Context) -> FAQsViewController {
// FAQsViewController()
// }
//
// func updateUIViewController(
// _ uiViewController: FAQsViewController,
// context: Context
// ) {}
//
// static var previews: some SwiftUI.View {
// let supportedLocales: [Locale] = [
// "en_US",
// "ko_KR"
// ].map(Locale.init(identifier:))
// return ForEach(supportedLocales, id: \.identifier) { locale in
// FAQsViewController_Preview()
// .environment(\.locale, locale)
// }
// }
//}
//#endif