Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DiffableDataSource + Compositional Layout으로 마이그레이션 #58

Merged
merged 14 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 38 additions & 40 deletions Projects/Features/Falling/Src/Home/FallingHomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import FallingInterface

final class FallingHomeViewController: TFBaseViewController {
private let viewModel: FallingHomeViewModel
private var dataSource: UICollectionViewDiffableDataSource<FallingProfileSection, FallingUser>!
private var dataSource: DataSource!
private lazy var homeView = FallingHomeView()

init(viewModel: FallingHomeViewModel) {
Expand All @@ -36,16 +36,18 @@ final class FallingHomeViewController: TFBaseViewController {
let mindImageView = UIImageView(image: DSKitAsset.Image.Icons.mind.image)
let mindImageItem = UIBarButtonItem(customView: mindImageView)

let notificationButtonItem = UIBarButtonItem(image: DSKitAsset.Image.Icons.bell.image, style: .plain, target: nil, action: nil)
let notificationButtonItem = UIBarButtonItem.noti
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIBarButtonItem 정의되어 있는게 있는지 몰랐네요


navigationItem.leftBarButtonItem = mindImageItem
navigationItem.rightBarButtonItem = notificationButtonItem
}

override func bindViewModel() {
let timeOverSubject = PublishSubject<Void>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. subject를 추가
  2. Cell에 observer로 전달
  3. Cell에서 timeOver signal 전송
  4. VC에서 subject Stream 구독해서 VC's VM으로 전달
  5. VC's VM에서 safe index 계산 후, output 전송
  6. VC에서 View에서 ScrollEvent 수행

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bindViewModel에서 timeOver 트리거를 PublishSubject로 만들어서 cell에서 timeZero와 바인딩하는 방식 좋은 거 같습니다!


let initialTrigger = Driver<Void>.just(())
let timerOverTrigger = self.rx.timeOverTrigger.asDriver()
let timerOverTrigger = timeOverSubject.asDriverOnErrorJustEmpty()

let viewWillAppearTrigger = self.rx.viewWillAppear.map { _ in true }.asDriverOnErrorJustEmpty()
let viewWillDisAppearTrigger = self.rx.viewWillDisAppear.map { _ in false }.asDriverOnErrorJustEmpty()
let timerActiveRelay = BehaviorRelay(value: true)
Expand All @@ -56,7 +58,7 @@ final class FallingHomeViewController: TFBaseViewController {
.when(.recognized)
.withLatestFrom(timerActiveRelay) { !$1 }
.asDriverOnErrorJustEmpty()

cardDoubleTapTrigger
.drive(timerActiveRelay)
.disposed(by: disposeBag)
Expand All @@ -65,61 +67,57 @@ final class FallingHomeViewController: TFBaseViewController {
.drive(timerActiveRelay)
.disposed(by: disposeBag)

let input = FallingHomeViewModel.Input(initialTrigger: initialTrigger,
timeOverTrigger: timerOverTrigger)
let input = FallingHomeViewModel.Input(
initialTrigger: initialTrigger,
timeOverTrigger: timerOverTrigger)

let output = viewModel.transform(input: input)

var usersCount = 0

let profileCellRegistration = UICollectionView.CellRegistration<FallingUserCollectionViewCell, FallingUser> { [weak self] cell, indexPath, item in
let observer = FallingUserCollectionViewCellObserver(
userCardScrollIndex: output.userCardScrollIndex.asObservable(),
timerActiveTrigger: timerActiveRelay.asObservable()
let profileCellRegistration = UICollectionView.CellRegistration<CellType, ModelType> { cell, indexPath, item in
let timerActiveTrigger = Driver.combineLatest(
output.userCardScrollIndex,
timerActiveRelay.asDriver()
)
.filter { indexPath.row == $0.0 }
.map { $0.1 }
.debug("cell timer active")

cell.bind(
FallinguserCollectionViewCellModel(userDomain: item),
timerActiveTrigger,
scrollToNextObserver: timeOverSubject
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Cell ViewModel 만들어서 주입
  • timerActive 트리거 주입 및 combineLatest로 변경


cell.bind(model: item)
cell.bind(observer,
index: indexPath,
usersCount: usersCount)
cell.delegate = self
}

dataSource = UICollectionViewDiffableDataSource(collectionView: homeView.collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
dataSource = DataSource(collectionView: homeView.collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: profileCellRegistration, for: indexPath, item: itemIdentifier)
})

output.userList
.drive(with: self, onNext: { this, list in
usersCount = list.count
var snapshot = NSDiffableDataSourceSnapshot<FallingProfileSection, FallingUser>()
var snapshot = Snapshot()
snapshot.appendSections([.profile])
snapshot.appendItems(list)
this.dataSource.apply(snapshot)
}).disposed(by: disposeBag)

output.userCardScrollIndex
output.nextCardIndex
.drive(with: self, onNext: { this, index in
if usersCount == 0 { return }
let index = index >= usersCount ? usersCount - 1 : index
let indexPath = IndexPath(row: index, section: 0)
this.homeView.collectionView.scrollToItem(at: indexPath,
at: .top,
animated: true)
})
this.homeView.collectionView.scrollToItem(
at: index,
at: .top,
animated: true)})
.disposed(by: self.disposeBag)
}
}

extension FallingHomeViewController: TimeOverDelegate {
@objc func scrollToNext() { }
}
// MARK: DiffableDataSource

extension Reactive where Base: FallingHomeViewController {
var timeOverTrigger: ControlEvent<Void> {
let source = methodInvoked(#selector(Base.scrollToNext)).map { _ in }
return ControlEvent(events: source)
}
extension FallingHomeViewController {
typealias CellType = FallingUserCollectionViewCell
typealias ModelType = FallingUser
typealias SectionType = FallingProfileSection
typealias DataSource = UICollectionViewDiffableDataSource<SectionType, ModelType>
typealias Snapshot = NSDiffableDataSourceSnapshot<SectionType, ModelType>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typealias로 정의해 놓는게 깔끔하네요

}

//#if DEBUG
Expand Down
45 changes: 29 additions & 16 deletions Projects/Features/Falling/Src/Home/FallingHomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,66 @@ import FallingInterface

import RxSwift
import RxCocoa
import Foundation

final class FallingHomeViewModel: ViewModelType {

private let fallingUseCase: FallingUseCaseInterface
// weak var delegate: FallingHomeDelegate?

// weak var delegate: FallingHomeDelegate?

var disposeBag: DisposeBag = DisposeBag()

struct Input {
let initialTrigger: Driver<Void>
let timeOverTrigger: Driver<Void>
}

struct Output {
let userList: Driver<[FallingUser]>
let userCardScrollIndex: Driver<Int>
let nextCardIndex: Driver<IndexPath>
}

init(fallingUseCase: FallingUseCaseInterface) {
self.fallingUseCase = fallingUseCase
}

func transform(input: Input) -> Output {
let currentIndexRelay = BehaviorRelay<Int>(value: 0)
let timeOverTrigger = input.timeOverTrigger

let usersResponse = input.initialTrigger
.flatMapLatest { [unowned self] _ in
self.fallingUseCase.user(alreadySeenUserUUIDList: [], userDailyFallingCourserIdx: 1, size: 100)
.asDriver(onErrorJustReturn: .init(selectDailyFallingIdx: 0, topicExpirationUnixTime: 0, userInfos: []))
}

let userList = usersResponse.map { $0.userInfos }.asDriver()


let userCount = userList.map { $0.count }

let userListObservable = userList.map { _ in
currentIndexRelay.accept(currentIndexRelay.value)
}

let nextScrollIndex = timeOverTrigger.withLatestFrom(currentIndexRelay.asDriver(onErrorJustReturn: 0)) { _, index in
currentIndexRelay.accept(index + 1)
}

let userCardScrollIndex = Driver.merge(userListObservable, nextScrollIndex).withLatestFrom(currentIndexRelay.asDriver(onErrorJustReturn: 0))


let userCardScrollIndex = Driver.merge(userListObservable, nextScrollIndex).withLatestFrom(userCount) { _, count in
let currentIndex = currentIndexRelay.value
return currentIndex >= count ? count - 1 : currentIndex
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index가 list count 보다 크거나 같을 때의 조건은 scroll될 때만 예외처리를 해놓은 것입니다.
index가 count -1로 계속 갱신하면 마지막 유저의 타이머가 계속 돕니다.
index의 값은 계속 +1을 하되, count와 같아지는 순간에만 예외적으로 return하는 조건이라 기존 처리를 유지해야 할 거 같습니다.

}

let nextCardIndex = userCardScrollIndex
.filter { $0 != 0 }
.map { IndexPath(row: $0, section: 0) }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VC indexPath 로직 VM으로 이동 및 명시적으로 output 분리 ( SRP, 유지보수

return Output(
userList: userList,
userCardScrollIndex: userCardScrollIndex)
userCardScrollIndex: userCardScrollIndex,
nextCardIndex: nextCardIndex
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,8 @@ struct FallingUserCollectionViewCellObserver {
var timerActiveTrigger: Observable<Bool>
}

@objc protocol TimeOverDelegate: AnyObject {
@objc func scrollToNext()
}

final class FallingUserCollectionViewCell: TFBaseCollectionViewCell {

var viewModel: FallinguserCollectionViewCellModel!
weak var delegate: TimeOverDelegate? = nil

lazy var profileCarouselView = ProfileCarouselView()

lazy var cardTimeView = CardTimeView()
Expand All @@ -48,44 +41,42 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
// profileCarouselView.tagCollectionView.isHidden = true
}

func bind(model: FallingUser) {
viewModel = FallinguserCollectionViewCellModel(userDomain: model)
profileCarouselView.bind(viewModel.userDomain)
}

func bind(_ observer: FallingUserCollectionViewCellObserver,
index: IndexPath,
usersCount: Int) {

let timerActiveTrigger =
observer.userCardScrollIndex
.observe(on: MainScheduler.asyncInstance)
.withLatestFrom(observer.timerActiveTrigger) { userCardScrollIndex, timerActiveTrigger in
if index.row == userCardScrollIndex {
self.profileCarouselView.hiddenDimView()
return observer.timerActiveTrigger
}
else { return Observable.just(false) }
}.map { $0 }
.flatMapLatest { return $0 }

let input = FallinguserCollectionViewCellModel.Input(timerActiveTrigger: timerActiveTrigger.asDriver(onErrorJustReturn: false))


func bind<O>(
_ viewModel: FallinguserCollectionViewCellModel,
_ timerTrigger: Driver<Bool>,
scrollToNextObserver: O) where O: ObserverType, O.Element == Void
{
let input = FallinguserCollectionViewCellModel.Input(timerActiveTrigger: timerTrigger)

let output = viewModel
.transform(input: input)

output.timeState
.drive(self.rx.timeState)
.disposed(by: self.disposeBag)


output.isDimViewHidden
.drive(with: self, onNext: { owner, isHidden in
if isHidden {
owner.profileCarouselView.hiddenDimView()
} else {
owner.profileCarouselView.showDimView()
}
})
.disposed(by: disposeBag)

output.timeZero
.do { [weak self] _ in self?.delegate?.scrollToNext() }
.drive()
.disposed(by: self.disposeBag)

.drive(scrollToNextObserver)
.disposed(by: disposeBag)

output.user
.drive(with: self, onNext: { owner, user in
owner.profileCarouselView.bind(user)
})
.disposed(by: disposeBag)

profileCarouselView.infoButton.rx.tap.asDriver()
.scan(true) { lastValue, _ in
return !lastValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ final class FallinguserCollectionViewCellModel: ViewModelType {

struct Output {
let timeState: Driver<TimeState>
let timeZero: Driver<Double>
let timeZero: Driver<Void>
let user: Driver<FallingUser>
let isDimViewHidden: Driver<Bool>
}

func transform(input: Input) -> Output {
Expand Down Expand Up @@ -161,12 +162,18 @@ final class FallinguserCollectionViewCellModel: ViewModelType {
}.asDriver(onErrorJustReturn: 8.0)

let timeState = timer.map { TimeState(rawValue: $0) }
let timeZero = timer.filter { $0 == 0 }

let timeZero = timer.filter { $0 == 0 }.map { _ in }

let isDimViewHidden = Driver.merge(
timerActiveTrigger.take(1).asDriverOnErrorJustEmpty(), // take(1)을 해주지 않으면 일시정지 때마다 dimmed 됨
timeZero.map { _ in false }
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DimView hidden state 관리 output
take(1)한 이유는 첫번째 timerTrigger만 capture하기 위해서, 지우면 일시정지할 때 마다 dimView state 변경됨

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 방법이 있는지 몰랐네요!


return Output(
timeState: timeState,
timeZero: timeZero,
user: user
user: user,
isDimViewHidden: isDimViewHidden
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import DSKit
final class ProfileCollectionViewCell: UICollectionViewCell {
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleToFill
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 scale보다는 AspectScale이 비율 유지할 수 있을 것 같아서 변경
aspectFit에서 fill로 변경한 이유 알려주시면 감사하겠습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scaleToFill 사용한 이유는 사실 이미지가 꽉 채워졌을 때를 확인하려고 썼던 거긴 해요. 고정으로 할 생각은 없었습니다.
scaleAspectFill을 하게 되면 비율 그대로 확대하다보니, 이미지가 짤립니다.
추후에는 이미지를 비율대로 자르고 확대하는게 어떨까 싶네요
생각해보니까 자르고 확대하나 비율에 맞게 스케일을 키우는 게 똑같은거 같긴하네요

return imageView
}()
Expand Down