diff --git a/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift b/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift index 51b6b842..720b15a7 100644 --- a/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift +++ b/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift @@ -11,6 +11,12 @@ import Core import DSKit import FallingInterface +enum FallingCellButtonAction { + case info(IndexPath) + case refuse(IndexPath) + case like(IndexPath) +} + final class FallingHomeViewController: TFBaseViewController { private let viewModel: FallingHomeViewModel private var dataSource: DataSource! @@ -47,24 +53,25 @@ final class FallingHomeViewController: TFBaseViewController { let initialTrigger = Driver.just(()) let timerOverTrigger = timeOverSubject.asDriverOnErrorJustEmpty() + let fallingCellButtonAction = PublishSubject() let viewWillDisAppearTrigger = self.rx.viewWillDisAppear.map { _ in false }.asDriverOnErrorJustEmpty() let timerActiveRelay = BehaviorRelay(value: true) - let cardDoubleTapTrigger = self.homeView.collectionView.rx - .tapGesture(configuration: { gestureRecognizer, delegate in - gestureRecognizer.numberOfTapsRequired = 2 - }) - .when(.recognized) + let profileDoubleTapTriggerObserver = PublishSubject() + + let profileDoubleTapTrigger = profileDoubleTapTriggerObserver .withLatestFrom(timerActiveRelay) { !$1 } .asDriverOnErrorJustEmpty() - Driver.merge(cardDoubleTapTrigger, viewWillDisAppearTrigger) + Driver.merge(profileDoubleTapTrigger, viewWillDisAppearTrigger) .drive(timerActiveRelay) .disposed(by: disposeBag) let input = FallingHomeViewModel.Input( initialTrigger: initialTrigger, - timeOverTrigger: timerOverTrigger) + timeOverTrigger: timerOverTrigger, + cellButtonAction: fallingCellButtonAction.asDriverOnErrorJustEmpty() + ) let output = viewModel.transform(input: input) @@ -78,8 +85,10 @@ final class FallingHomeViewController: TFBaseViewController { cell.bind( FallinguserCollectionViewCellModel(userDomain: item), - timerActiveTrigger, - scrollToNextObserver: timeOverSubject + timerActiveTrigger: timerActiveTrigger, + timeOverSubject: timeOverSubject, + profileDoubleTapTriggerObserver: profileDoubleTapTriggerObserver, + fallingCellButtonAction: fallingCellButtonAction ) } @@ -117,6 +126,14 @@ final class FallingHomeViewController: TFBaseViewController { animated: true )}) .disposed(by: self.disposeBag) + + output.info + .drive(with: self) { owner, indexPath in + guard let cell = owner.homeView.collectionView.cellForItem(at: indexPath) as? FallingUserCollectionViewCell + else { return } + cell.userInfoCollectionView.isHidden.toggle() + } + .disposed(by: disposeBag) } } diff --git a/Projects/Features/Falling/Src/Home/FallingHomeViewModel.swift b/Projects/Features/Falling/Src/Home/FallingHomeViewModel.swift index ef1a0ffc..c339a2a6 100644 --- a/Projects/Features/Falling/Src/Home/FallingHomeViewModel.swift +++ b/Projects/Features/Falling/Src/Home/FallingHomeViewModel.swift @@ -23,11 +23,13 @@ final class FallingHomeViewModel: ViewModelType { struct Input { let initialTrigger: Driver let timeOverTrigger: Driver + let cellButtonAction: Driver } struct Output { let userList: Driver<[FallingUser]> let nextCardIndexPath: Driver + let info: Driver } init(fallingUseCase: FallingUseCaseInterface) { @@ -59,10 +61,18 @@ final class FallingHomeViewModel: ViewModelType { updateScrollIndexTrigger ).withLatestFrom(currentIndexRelay.asDriver(onErrorJustReturn: 0) .map { IndexPath(row: $0, section: 0) }) + + let info = input.cellButtonAction + .compactMap { action -> IndexPath? in + if case let .info(indexPath) = action { + return indexPath + } else { return nil } + } return Output( userList: userList, - nextCardIndexPath: nextCardIndexPath + nextCardIndexPath: nextCardIndexPath, + info: info ) } } diff --git a/Projects/Features/Falling/Src/Subviews/Cell/FallingUserCollectionViewCell.swift b/Projects/Features/Falling/Src/Subviews/Cell/FallingUserCollectionViewCell.swift index 28e95557..dc49f7e8 100644 --- a/Projects/Features/Falling/Src/Subviews/Cell/FallingUserCollectionViewCell.swift +++ b/Projects/Features/Falling/Src/Subviews/Cell/FallingUserCollectionViewCell.swift @@ -19,6 +19,15 @@ struct FallingUserCollectionViewCellObserver { final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { private var dataSource: DataSource! + private var indexPath: IndexPath? { + guard let collectionView = self.superview as? UICollectionView, + let indexPath = collectionView.indexPath(for: self) else { + TFLogger.ui.error("indexPath 얻기 실패") + return nil + } + return indexPath + } + lazy var profileCollectionView: TFBaseCollectionView = { let layout = UICollectionViewCompositionalLayout.horizontalListLayout() @@ -32,14 +41,6 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { return collectionView }() - var photos: [UserProfilePhoto] = [] { - didSet { - userInfoBoxView.pageControl.currentPage = 0 - userInfoBoxView.pageControl.numberOfPages = oldValue.count -// collectionView.reloadData() - } - } - lazy var userInfoBoxView = UserInfoBoxView() lazy var cardTimeView = CardTimeView() @@ -56,12 +57,22 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { return pauseView }() + lazy var userInfoCollectionView: UserInfoCollectionView = { + let collectionView = UserInfoCollectionView() + collectionView.layer.cornerRadius = 20 + collectionView.clipsToBounds = true + collectionView.collectionView.backgroundColor = DSKitAsset.Color.DimColor.default.color + collectionView.isHidden = true + return collectionView + }() + override func makeUI() { self.layer.cornerRadius = 20 self.contentView.addSubview(profileCollectionView) self.contentView.addSubview(cardTimeView) self.contentView.addSubview(userInfoBoxView) + self.contentView.addSubview(userInfoCollectionView) self.contentView.addSubview(pauseView) profileCollectionView.snp.makeConstraints { @@ -74,17 +85,24 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { } self.userInfoBoxView.snp.makeConstraints { + $0.height.equalTo(145) $0.leading.trailing.equalToSuperview().inset(16) $0.bottom.equalToSuperview().inset(12) } + userInfoCollectionView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(10) + $0.height.equalTo(300) + $0.bottom.equalTo(userInfoBoxView.snp.top).offset(-8) + } + self.pauseView.snp.makeConstraints { $0.edges.equalToSuperview() } - self.configureDataSource() - self.profileCollectionView.showDimView() + + self.setDataSource() } override func prepareForReuse() { @@ -94,10 +112,14 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { func bind( _ viewModel: FallinguserCollectionViewCellModel, - _ timerTrigger: Driver, - scrollToNextObserver: O - ) where O: ObserverType, O.Element == Void { - let input = FallinguserCollectionViewCellModel.Input(timerActiveTrigger: timerTrigger) + timerActiveTrigger: Driver, + timeOverSubject: PublishSubject, + profileDoubleTapTriggerObserver: PublishSubject, + fallingCellButtonAction: O + ) where O: ObserverType, O.Element == FallingCellButtonAction { + let input = FallinguserCollectionViewCellModel.Input( + timerActiveTrigger: timerActiveTrigger + ) let output = viewModel .transform(input: input) @@ -111,36 +133,89 @@ final class FallingUserCollectionViewCell: TFBaseCollectionViewCell { .disposed(by: self.disposeBag) output.timeStart - .drive(with: self, onNext: { owner, _ in + .drive(with: self) { owner, _ in owner.profileCollectionView.hiddenDimView() - }) + } .disposed(by: disposeBag) output.timeZero - .drive(scrollToNextObserver) + .drive(timeOverSubject) .disposed(by: disposeBag) output.isTimerActive .drive(pauseView.rx.isHidden) .disposed(by: disposeBag) - userInfoBoxView.infoButton.rx.tap.asDriver() - .scan(true) { lastValue, _ in - return !lastValue + let profileDoubleTapTrigger = self.profileCollectionView.rx + .tapGesture(configuration: { gestureRecognizer, delegate in + gestureRecognizer.numberOfTapsRequired = 2 + }) + .when(.recognized) + .mapToVoid() + .asDriverOnErrorJustEmpty() + + let pauseViewDoubleTapTrigger = self.pauseView.rx + .tapGesture(configuration: { gestureRecognizer, delegate in + gestureRecognizer.numberOfTapsRequired = 2 + }) + .when(.recognized) + .mapToVoid() + .asDriverOnErrorJustEmpty() + + Driver.merge(profileDoubleTapTrigger, pauseViewDoubleTapTrigger) + .map { _ in } + .drive(profileDoubleTapTriggerObserver) + .disposed(by: disposeBag) + + profileCollectionView.rx.didEndDisplayingCell.asDriver() + .debug() + .drive(with: self) { owner, indexPath in + self.userInfoBoxView.pageControl.currentPage } - .drive(userInfoBoxView.tagCollectionView.rx.isHidden) .disposed(by: disposeBag) + + userInfoBoxView.infoButton.rx.tap.asDriver() + .scan(true, accumulator: { value, _ in + return !value + }) + .drive(userInfoCollectionView.rx.isHidden) + .disposed(by: disposeBag) + + userInfoBoxView.refuseButton.rx.tapGesture() + .when(.recognized) + .compactMap { [weak self] _ in self?.indexPath } + .map { FallingCellButtonAction.refuse($0) } + .bind(to: fallingCellButtonAction) + .disposed(by: disposeBag) + + userInfoBoxView.likeButton.rx.tapGesture() + .when(.recognized) + .compactMap { [weak self] _ in self?.indexPath } + .map { FallingCellButtonAction.like($0) } + .bind(to: fallingCellButtonAction) + .disposed(by: disposeBag) + } + + func bind(userProfilePhotos: [UserProfilePhoto]) { + var snapshot = Snapshot() + snapshot.appendSections([.profile]) + snapshot.appendItems(userProfilePhotos) + self.dataSource.apply(snapshot) + + userInfoBoxView.pageControl.numberOfPages = userProfilePhotos.count } - func dotPosition(progress: Double, rect: CGRect) -> CGPoint { - var progress = progress - // progress가 -0.05미만 혹은 1이상은 점(dot)을 0초에 위치시키기 위함 - let strokeRange: Range = -0.05..<0.95 - if !(strokeRange ~= progress) { progress = 0.95 } + func dotPosition(progress: CGFloat, rect: CGRect) -> CGPoint { + let progress = round(progress * 100) / 100 // 오차를 줄이기 위함 let radius = CGFloat(rect.height / 2 - cardTimeView.timerView.strokeLayer.lineWidth / 2) - let angle = 2 * CGFloat.pi * CGFloat(progress) - CGFloat.pi / 2 - let dotX = radius * cos(angle + 0.35) - let dotY = radius * sin(angle + 0.35) + + var angle = 2 * CGFloat.pi * progress - CGFloat.pi / 2 + CGFloat.pi / 6 // 두 원의 중점과 원점이 이루는 각도를 30도로 가정 + if angle <= -CGFloat.pi / 2 || CGFloat.pi * 1.5 <= angle { + angle = -CGFloat.pi / 2 // 정점 각도 + } + + let dotX = radius * cos(angle) + let dotY = radius * sin(angle) let point = CGPoint(x: dotX, y: dotY) @@ -156,7 +231,7 @@ extension FallingUserCollectionViewCell { typealias DataSource = UICollectionViewDiffableDataSource typealias Snapshot = NSDiffableDataSourceSnapshot - func configureDataSource() { + func setDataSource() { let profileCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in cell.bind(imageURL: item.url) } @@ -165,13 +240,6 @@ extension FallingUserCollectionViewCell { return collectionView.dequeueConfiguredReusableCell(using: profileCellRegistration, for: indexPath, item: itemIdentifier) }) } - - func setupDataSource(userProfilePhotos: [UserProfilePhoto]) { - var snapshot = Snapshot() - snapshot.appendSections([.profile]) - snapshot.appendItems(userProfilePhotos) - self.dataSource.apply(snapshot) - } } extension Reactive where Base: FallingUserCollectionViewCell { @@ -188,10 +256,9 @@ extension Reactive where Base: FallingUserCollectionViewCell { base.cardTimeView.timerView.timerLabel.text = timeState.getText - base.cardTimeView.progressView.progress = CGFloat(timeState.getProgress) + base.cardTimeView.progressView.progress = timeState.getProgress - // TimerView Animation은 소수점 둘째 자리까지 표시해야 오차가 발생하지 않음 - let strokeEnd = round(CGFloat(timeState.getProgress) * 100) / 100 + let strokeEnd = timeState.getProgress base.cardTimeView.timerView.dotLayer.position = base.dotPosition(progress: strokeEnd, rect: base.cardTimeView.timerView.bounds) base.cardTimeView.timerView.strokeLayer.strokeEnd = strokeEnd @@ -203,9 +270,9 @@ extension Reactive where Base: FallingUserCollectionViewCell { var user: Binder { return Binder(self.base) { (base, user) in - base.photos = user.userProfilePhotos - base.setupDataSource(userProfilePhotos: user.userProfilePhotos) + base.bind(userProfilePhotos: user.userProfilePhotos) base.userInfoBoxView.bind(user) + base.userInfoCollectionView.bind(user) } } }