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 8 commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ extension FallingTarget {
],
"userProfilePhotos": [
{
"url": "test1",
"url": "https://cdn.pixabay.com/photo/2024/01/15/21/16/dog-8510901_640.jpg",
"priority": 1
},
{
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example2.png?alt=media&token=e19493ed-10ba-4784-bf94-f4b99af84161",
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example1.png?alt=media&token=dc28c0cd-98b2-4332-9660-35530283d77c",
"priority": 2
},
{
Expand Down Expand Up @@ -116,11 +116,11 @@ extension FallingTarget {
],
"userProfilePhotos": [
{
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example1 .png?alt=media&token=dc28c0cd-98b2-4332-9660-35530283d77c",
"url": "https://cdn.pixabay.com/photo/2023/05/07/11/57/surfboard-7976219_960_720.jpg",
"priority": 1
},
{
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example2.png?alt=media&token=e19493ed-10ba-4784-bf94-f4b99af84161",
"url": "https://cdn.pixabay.com/photo/2024/01/07/11/17/welsh-corgi-8492879_640.jpg",
"priority": 2
},
{
Expand Down Expand Up @@ -173,11 +173,11 @@ extension FallingTarget {
],
"userProfilePhotos": [
{
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example1.png?alt=media&token=dc28c0cd-98b2-4332-9660-35530283d77c",
"url": "https://cdn.pixabay.com/photo/2023/03/18/05/26/ferris-wheel-7859855_640.jpg",
"priority": 1
},
{
"url": "https://firebasestorage.googleapis.com/v0/b/tht-falling.appspot.com/o/images%2Fprofile_example2.png?alt=media&token=e19493ed-10ba-4784-bf94-f4b99af84161",
"url": "https://cdn.pixabay.com/photo/2023/11/17/12/46/cat-8394224_640.jpg",
"priority": 2
},
{
Expand Down
71 changes: 67 additions & 4 deletions Projects/Features/Falling/Src/Home/FallingHomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import DSKit

final class FallingHomeView: TFBaseView {
lazy var collectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.minimumLineSpacing = 14
flowLayout.scrollDirection = .vertical
let flowLayout = UICollectionViewCompositionalLayout.verticalListLayout(withEstimatedHeight: ((UIWindow.keyWindow?.frame.width ?? 0) - 32) * 1.64)
let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: flowLayout)
collectionView.isScrollEnabled = false
Expand All @@ -23,11 +21,76 @@ final class FallingHomeView: TFBaseView {
}()

override func makeUI() {
self.backgroundColor = DSKitAsset.Color.neutral700.color

self.addSubview(collectionView)

self.collectionView.snp.makeConstraints {
$0.top.equalToSuperview().inset(8)
$0.top.equalTo(self.safeAreaLayoutGuide).inset(8)
$0.leading.bottom.trailing.equalToSuperview()
}
}
}

extension UICollectionViewCompositionalLayout {
static func verticalListLayout(withEstimatedHeight estimatedHeight: CGFloat = 110) -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(section: .verticalListSection(withEstimatedHeight: estimatedHeight))
}

static func horizontalListLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(section: .horizontalListSection())
}
}

extension NSCollectionLayoutSection {
static func verticalListSection(withEstimatedHeight estimatedHeight: CGFloat = 110) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)

let layoutGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(estimatedHeight)
)

let layoutGroup = NSCollectionLayoutGroup.vertical(
layoutSize: layoutGroupSize,
subitems: [layoutItem]
)

let section = NSCollectionLayoutSection(group: layoutGroup)
section.contentInsets = NSDirectionalEdgeInsets(
top: 0,
leading: 16,
bottom: 0,
trailing: 16
)
section.interGroupSpacing = 14
return section
}

static func horizontalListSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)

let layoutGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)

let layoutGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: layoutGroupSize,
subitems: [layoutItem]
)

let section = NSCollectionLayoutSection(group: layoutGroup)
section.orthogonalScrollingBehavior = .paging

return section
}
}
95 changes: 41 additions & 54 deletions Projects/Features/Falling/Src/Home/FallingHomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ 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) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
setupDelegate()
}

required init?(coder: NSCoder) {
Expand All @@ -37,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 @@ -66,71 +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())

cell.bind(model: item)
cell.bind(observer,
index: indexPath,
usersCount: usersCount)
cell.delegate = self
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로 변경

}

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)
}

private func setupDelegate() {
homeView.collectionView.delegate = self
}
}

extension FallingHomeViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width - 32,
height: (view.frame.width - 32) * 1.64)
}
}

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 All @@ -141,9 +128,9 @@ extension Reactive where Base: FallingHomeViewController {
// static var previews: some View {
// let service = FallingAPI(isStub: true, sampleStatusCode: 200, customEndpointClosure: nil)
// let navigator = MainNavigator(controller: UINavigationController(), fallingService: service)
//
//
// let viewModel = MainViewModel(navigator: navigator, service: service)
//
//
// return FallingHomeViewController(viewModel: viewModel)
// .toPreView()
// }
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
)
}
}
Loading