From c8aa15f7976d6284d4fce428666fc7f863db8bcc Mon Sep 17 00:00:00 2001 From: LeeSeungmin Date: Sat, 7 Oct 2023 17:06:56 +0900 Subject: [PATCH] =?UTF-8?q?[#35]=20=ED=98=84=EC=9E=AC=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=8A=94=20=EC=9C=A0=EC=A0=80=20=EC=B9=B4=EB=93=9C=20=EC=85=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=EC=9D=98=20=ED=83=80=EC=9E=84=EC=9D=B4=200?= =?UTF-8?q?=EC=9D=B4=20=EB=90=98=EB=A9=B4=20=EC=9E=90=EB=8F=99=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=9D=8C=20=EC=85=80=EB=A1=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cell 모델 구현 - isTimeOver driver의 파라미터가 true가 되면 delegate로 scrollToNext함수를 호출 - delegate를 준수하는 vc에서 scrollToNext를 호출하면 timeOverTrigger event에서 void를 emit하도록 해서 timeOverTrigger가 emit할 때만 해당 셀에 대한 구독을 생성(bindViewModel) --- .../Main/Cell/MainCollectionViewCell.swift | 32 ++++- .../MainCollectionViewItemViewModel.swift | 132 ++++++++++++++++++ Falling/Sources/Feature/Main/MainView.swift | 1 + .../Feature/Main/MainViewController.swift | 47 ++++++- .../Sources/Feature/Main/MainViewModel.swift | 125 +++-------------- .../Feature/Main/Subviews/CardTimerView.swift | 1 + 6 files changed, 225 insertions(+), 113 deletions(-) create mode 100644 Falling/Sources/Feature/Main/Cell/MainCollectionViewItemViewModel.swift diff --git a/Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift b/Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift index 62dacda1..c0dc9733 100644 --- a/Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift +++ b/Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift @@ -8,8 +8,15 @@ import UIKit import RxSwift +@objc protocol TimeOverDelegate: AnyObject { + @objc func scrollToNext() +} + final class MainCollectionViewCell: TFBaseCollectionViewCell { + var viewModel: MainCollectionViewItemViewModel! + weak var delegate: TimeOverDelegate? + lazy var userImageView: UIImageView = { let imageView = UIImageView() imageView.image = .add @@ -86,6 +93,29 @@ final class MainCollectionViewCell: TFBaseCollectionViewCell { } } + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + + func setup(item: UserDTO) { + viewModel = MainCollectionViewItemViewModel(userDTO: item) + } + + func bindViewModel() { + let output = viewModel.transform(input: MainCollectionViewItemViewModel.Input()) + + output.timeState + .drive(self.rx.timeState) + .disposed(by: self.disposeBag) + + output.isTimeOver + .do { value in + if value { self.delegate?.scrollToNext() } + }.drive() + .disposed(by: self.disposeBag) + } + func dotPosition(progress: Double, rect: CGRect) -> CGPoint { var progress = progress // progress가 -0.05미만 혹은 1이상은 점(dot)을 0초에 위치시키기 위함 @@ -106,7 +136,7 @@ final class MainCollectionViewCell: TFBaseCollectionViewCell { } extension Reactive where Base: MainCollectionViewCell { - var timeState: Binder { + var timeState: Binder { return Binder(self.base) { (base, timeState) in base.timerView.trackLayer.strokeColor = timeState.fillColor.color.cgColor base.timerView.strokeLayer.strokeColor = timeState.color.color.cgColor diff --git a/Falling/Sources/Feature/Main/Cell/MainCollectionViewItemViewModel.swift b/Falling/Sources/Feature/Main/Cell/MainCollectionViewItemViewModel.swift new file mode 100644 index 00000000..3ea8f44a --- /dev/null +++ b/Falling/Sources/Feature/Main/Cell/MainCollectionViewItemViewModel.swift @@ -0,0 +1,132 @@ +// +// MainCollectionViewItemViewModel.swift +// Falling +// +// Created by SeungMin on 2023/10/06. +// + +import Foundation + +import RxSwift +import RxCocoa + +final class MainCollectionViewItemViewModel: ViewModelType { + + let userDTO: UserDTO + + init(userDTO: UserDTO) { + self.userDTO = userDTO + } + + enum TimeState { + case initial(value: Double) // 7~8 + case five(value: Double) // 6~7 + case four(value: Double) // 5~6 + case three(value: Double) // 4~5 + case two(value: Double) // 3~4 + case one(value: Double) // 2~3 + case zero(value: Double) // 1~2 + case over(value: Double) // 0~1 + + init(rawValue: Double) { + switch rawValue { + case 7.0..<8.0: + self = .initial(value: rawValue) + case 6.0..<7.0: + self = .five(value: rawValue) + case 5.0..<6.0: + self = .four(value: rawValue) + case 4.0..<5.0: + self = .three(value: rawValue) + case 3.0..<4.0: + self = .two(value: rawValue) + case 2.0..<3.0: + self = .one(value: rawValue) + case 1.0..<2.0: + self = .zero(value: rawValue) + default: + self = .over(value: rawValue) + } + } + + var color: FallingColors { + switch self { + case .zero, .five: + return FallingAsset.Color.primary500 + case .four: + return FallingAsset.Color.thtOrange100 + case .three: + return FallingAsset.Color.thtOrange200 + case .two: + return FallingAsset.Color.thtOrange300 + case .one: + return FallingAsset.Color.thtRed + default: + return FallingAsset.Color.neutral300 + } + } + + var isDotHidden: Bool { + switch self { + case .initial, .over: + return true + default: + return false + } + } + + var fillColor: FallingColors { + switch self { + case .over: + return FallingAsset.Color.neutral300 + default: + return FallingAsset.Color.clear + } + } + + var getText: String { + switch self { + case .initial, .over: + return String("-") + case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value): + return String(Int(value) - 1) + } + } + + var getProgress: Double { + switch self { + case .initial: + return 1 + case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value), .over(let value): + return (value - 2) / 5 + } + } + } + + var disposeBag: DisposeBag = DisposeBag() + + struct Input { + + } + + struct Output { + let timeState: Driver + let isTimeOver: Driver + } + + func transform(input: Input) -> Output { + let time = Observable.interval(.milliseconds(10), + scheduler: MainScheduler.instance) + .take(8 * 100 + 1) + .map { round((8 - Double($0) / 100) * 100) / 100 } + .asDriver(onErrorDriveWith: Driver.empty()) + + let timeState = time.map { TimeState(rawValue: $0) } + let isTimeOver = time.map { $0 == 0.0 } + + return Output( + timeState: timeState, + isTimeOver: isTimeOver + ) + } +} diff --git a/Falling/Sources/Feature/Main/MainView.swift b/Falling/Sources/Feature/Main/MainView.swift index cb2f5389..85c03f1d 100644 --- a/Falling/Sources/Feature/Main/MainView.swift +++ b/Falling/Sources/Feature/Main/MainView.swift @@ -19,6 +19,7 @@ final class MainView: TFBaseView { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) collectionView.register(cellType: MainCollectionViewCell.self) + collectionView.isScrollEnabled = false collectionView.backgroundColor = FallingAsset.Color.neutral700.color return collectionView }() diff --git a/Falling/Sources/Feature/Main/MainViewController.swift b/Falling/Sources/Feature/Main/MainViewController.swift index 9554c0c3..9a950227 100644 --- a/Falling/Sources/Feature/Main/MainViewController.swift +++ b/Falling/Sources/Feature/Main/MainViewController.swift @@ -7,6 +7,7 @@ import UIKit +import RxSwift import RxCocoa import RxDataSources import SwiftUI @@ -46,25 +47,48 @@ final class MainViewController: TFBaseViewController { override func bindViewModel() { let initialTrigger = self.rx.viewWillAppear.map { _ in }.asDriverOnErrorJustEmpty() - let output = viewModel.transform(input: MainViewModel.Input(trigger: initialTrigger)) + let timerOverTrigger = self.rx.timeOverTrigger.map { _ in + }.asDriverOnErrorJustEmpty() + + let output = viewModel.transform(input: MainViewModel.Input(trigger: initialTrigger, timeOverTrigger: timerOverTrigger)) + + var count = 0 + output.userList + .drive { userSection in + count = userSection[0].items.count + }.disposed(by: disposeBag) + - // output.state - // .drive(mainView.rx.timeState) - // .disposed(by: disposeBag) let dataSource = RxCollectionViewSectionedAnimatedDataSource { dataSource, collectionView, indexPath, item in let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: MainCollectionViewCell.self) - output.timeState - .drive(cell.rx.timeState) + cell.setup(item: item) + output.currentPage + .do { index in + if index == indexPath.row { + cell.bindViewModel() + } + }.drive() .disposed(by: self.disposeBag) + cell.delegate = self return cell } output.userList .drive(mainView.collectionView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag) + + output.currentPage + .do(onNext: { index in + let index = index >= count ? count - 1 : index + let indexPath = IndexPath(row: index, section: 0) + self.mainView.collectionView.scrollToItem(at: indexPath, + at: .top, + animated: true) + }).drive() + .disposed(by: self.disposeBag) } private func setupDelegate() { @@ -79,6 +103,17 @@ extension MainViewController: UICollectionViewDelegateFlowLayout { } } +extension MainViewController: TimeOverDelegate { + @objc func scrollToNext() { } +} + +extension Reactive where Base: MainViewController { + var timeOverTrigger: ControlEvent { + let source = methodInvoked(#selector(Base.scrollToNext)).map { _ in } + return ControlEvent(events: source) + } +} + struct MainViewControllerPreView: PreviewProvider { static var previews: some View { MainViewController(viewModel: MainViewModel(navigator: MainNavigator(controller: UINavigationController()))).toPreView() diff --git a/Falling/Sources/Feature/Main/MainViewModel.swift b/Falling/Sources/Feature/Main/MainViewModel.swift index 05aceb70..b61423f3 100644 --- a/Falling/Sources/Feature/Main/MainViewModel.swift +++ b/Falling/Sources/Feature/Main/MainViewModel.swift @@ -7,105 +7,20 @@ import RxSwift import RxCocoa -import Foundation final class MainViewModel: ViewModelType { - enum TimeState { - case initial(value: Double) // 7~8 - case five(value: Double) // 6~7 - case four(value: Double) // 5~6 - case three(value: Double) // 4~5 - case two(value: Double) // 3~4 - case one(value: Double) // 2~3 - case zero(value: Double) // 1~2 - case over(value: Double) // 0~1 - - init(rawValue: Double) { - switch rawValue { - case 7.0..<8.0: - self = .initial(value: rawValue) - case 6.0..<7.0: - self = .five(value: rawValue) - case 5.0..<6.0: - self = .four(value: rawValue) - case 4.0..<5.0: - self = .three(value: rawValue) - case 3.0..<4.0: - self = .two(value: rawValue) - case 2.0..<3.0: - self = .one(value: rawValue) - case 1.0..<2.0: - self = .zero(value: rawValue) - default: - self = .over(value: rawValue) - } - } - - var color: FallingColors { - switch self { - case .zero, .five: - return FallingAsset.Color.primary500 - case .four: - return FallingAsset.Color.thtOrange100 - case .three: - return FallingAsset.Color.thtOrange200 - case .two: - return FallingAsset.Color.thtOrange300 - case .one: - return FallingAsset.Color.thtRed - default: - return FallingAsset.Color.neutral300 - } - } - - var isDotHidden: Bool { - switch self { - case .initial, .over: - return true - default: - return false - } - } - - var fillColor: FallingColors { - switch self { - case .over: - return FallingAsset.Color.neutral300 - default: - return FallingAsset.Color.clear - } - } - - var getText: String { - switch self { - case .initial, .over: - return String("-") - case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value): - return String(Int(value) - 1) - } - } - - var getProgress: Double { - switch self { - case .initial: - return 1 - case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value), .over(let value): - return (value - 2) / 5 - } - } - } - private let navigator: MainNavigator var disposeBag: DisposeBag = DisposeBag() struct Input { let trigger: Driver + let timeOverTrigger: Driver } struct Output { let userList: Driver<[UserSection]> - let timeState: Driver + let currentPage: Driver } init(navigator: MainNavigator) { @@ -114,33 +29,31 @@ final class MainViewModel: ViewModelType { func transform(input: Input) -> Output { let listSubject = BehaviorSubject<[UserSection]>(value: []) + let currentIndex = BehaviorSubject(value: 0) - let userSections = [UserSection(header: "ass", - items: [ - UserDTO(userIdx: 0), - UserDTO(userIdx: 1), - UserDTO(userIdx: 2), - ])] - -// let userList = listSubject.onNext(userSections) + let timeOverTrigger = input.timeOverTrigger -// let refreshResponse = input.trigger.map { -// listSubject.onNext(userSections) -// } + let userSectionList = [UserSection(header: "header", + items: [ + UserDTO(userIdx: 0), + UserDTO(userIdx: 1), + UserDTO(userIdx: 2), + ])] - let userList = Observable.just(userSections).asDriver(onErrorJustReturn: []) + let userList = Observable.just(userSectionList).asDriver(onErrorJustReturn: []) - let time = Observable.interval(.milliseconds(10), - scheduler: MainScheduler.instance) - .take(8 * 100 + 1) - .map { round((8 - Double($0) / 100) * 100) / 100 } - .asDriver(onErrorDriveWith: Driver.empty()) + let currentPage = currentIndex.map{ $0 }.asDriver(onErrorJustReturn: 0) - let timeState = time.map { TimeState(rawValue: $0) } + timeOverTrigger.do(onNext: { + do { + currentIndex.onNext(try currentIndex.value() + 1) + } catch { } + }).drive() + .disposed(by: disposeBag) return Output( userList: userList, - timeState: timeState + currentPage: currentPage ) } } diff --git a/Falling/Sources/Feature/Main/Subviews/CardTimerView.swift b/Falling/Sources/Feature/Main/Subviews/CardTimerView.swift index 7bbccee1..a4e267bb 100644 --- a/Falling/Sources/Feature/Main/Subviews/CardTimerView.swift +++ b/Falling/Sources/Feature/Main/Subviews/CardTimerView.swift @@ -23,6 +23,7 @@ final class CardTimerView: TFBaseView { let layer = CAShapeLayer() layer.lineWidth = 2 layer.fillColor = FallingAsset.Color.clear.color.cgColor + layer.strokeColor = FallingAsset.Color.neutral300.color.cgColor return layer }()