Skip to content

Commit

Permalink
[#35] 현재 보이는 유저 카드 셀에서의 타임이 0이 되면 자동으로 다음 셀로 스크롤 구현
Browse files Browse the repository at this point in the history
- cell 모델 구현
- isTimeOver driver의 파라미터가 true가 되면 delegate로 scrollToNext함수를 호출
- delegate를 준수하는 vc에서 scrollToNext를 호출하면 timeOverTrigger event에서 void를 emit하도록 해서
timeOverTrigger가 emit할 때만 해당 셀에 대한 구독을 생성(bindViewModel)
  • Loading branch information
Minny27 committed Oct 7, 2023
1 parent 1447acf commit c8aa15f
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 113 deletions.
32 changes: 31 additions & 1 deletion Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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초에 위치시키기 위함
Expand All @@ -106,7 +136,7 @@ final class MainCollectionViewCell: TFBaseCollectionViewCell {
}

extension Reactive where Base: MainCollectionViewCell {
var timeState: Binder<MainViewModel.TimeState> {
var timeState: Binder<MainCollectionViewItemViewModel.TimeState> {
return Binder(self.base) { (base, timeState) in
base.timerView.trackLayer.strokeColor = timeState.fillColor.color.cgColor
base.timerView.strokeLayer.strokeColor = timeState.color.color.cgColor
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TimeState>
let isTimeOver: Driver<Bool>
}

func transform(input: Input) -> Output {
let time = Observable<Int>.interval(.milliseconds(10),
scheduler: MainScheduler.instance)
.take(8 * 100 + 1)
.map { round((8 - Double($0) / 100) * 100) / 100 }
.asDriver(onErrorDriveWith: Driver<Double>.empty())

let timeState = time.map { TimeState(rawValue: $0) }
let isTimeOver = time.map { $0 == 0.0 }

return Output(
timeState: timeState,
isTimeOver: isTimeOver
)
}
}
1 change: 1 addition & 0 deletions Falling/Sources/Feature/Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}()
Expand Down
47 changes: 41 additions & 6 deletions Falling/Sources/Feature/Main/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import UIKit

import RxSwift
import RxCocoa
import RxDataSources
import SwiftUI
Expand Down Expand Up @@ -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<UserSection> { 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() {
Expand All @@ -79,6 +103,17 @@ extension MainViewController: UICollectionViewDelegateFlowLayout {
}
}

extension MainViewController: TimeOverDelegate {
@objc func scrollToNext() { }
}

extension Reactive where Base: MainViewController {
var timeOverTrigger: ControlEvent<Void> {
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()
Expand Down
Loading

0 comments on commit c8aa15f

Please sign in to comment.