diff --git a/Projects/Domain/Src/Model/User/EmojiType.swift b/Projects/Domain/Src/Model/User/EmojiType.swift index dba54748..2fe4bd1f 100644 --- a/Projects/Domain/Src/Model/User/EmojiType.swift +++ b/Projects/Domain/Src/Model/User/EmojiType.swift @@ -8,7 +8,7 @@ import Foundation // MARK: - List -public struct EmojiType { +public struct EmojiType: Hashable { public let identifier = UUID() public let idx: Int public let name, emojiCode: String @@ -19,3 +19,12 @@ public struct EmojiType { self.emojiCode = emojiCode } } + +extension EmojiType { + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } + public static func == (lhs: EmojiType, rhs: EmojiType) -> Bool { + lhs.identifier == rhs.identifier + } +} diff --git a/Projects/Features/Falling/Interface/Src/Model/FallingUser.swift b/Projects/Features/Falling/Interface/Src/Model/FallingUser.swift index c57be9e3..102d0ced 100644 --- a/Projects/Features/Falling/Interface/Src/Model/FallingUser.swift +++ b/Projects/Features/Falling/Interface/Src/Model/FallingUser.swift @@ -12,6 +12,38 @@ public enum FallingProfileSection { case profile } +public enum FallingUserInfoSection: Int { + case interest + case ideal + case introduction + + public var title: String { + switch self { + case .interest: + return "내 관심사" + case .ideal: + return "내 이상형" + case .introduction: + return "자기소개" + } + } +} + +public enum FallingUserInfoItem: Hashable { + case interest(EmojiType) + case ideal(EmojiType) + case introduction(String) + + public var item: Any { + switch self { + case .interest(let item), .ideal(let item): + return item + case .introduction(let item): + return item + } + } +} + public struct FallingUserInfo { public let selectDailyFallingIdx, topicExpirationUnixTime: Int public let userInfos: [FallingUser] diff --git a/Projects/Features/Falling/Src/Home/FallingHomeView.swift b/Projects/Features/Falling/Src/Home/FallingHomeView.swift index 804a3056..4bd27e49 100644 --- a/Projects/Features/Falling/Src/Home/FallingHomeView.swift +++ b/Projects/Features/Falling/Src/Home/FallingHomeView.swift @@ -10,10 +10,6 @@ import UIKit import Core import DSKit -enum ElementKind: String { - case badge, header, footer -} - final class FallingHomeView: TFBaseView { lazy var collectionView: UICollectionView = { let flowLayout = UICollectionViewCompositionalLayout.verticalListLayout(withEstimatedHeight: ((UIWindow.keyWindow?.frame.width ?? 0) - 32) * 1.64) @@ -69,7 +65,7 @@ extension NSCollectionLayoutSection { heightDimension: .estimated(((UIWindow.keyWindow?.frame.width ?? 0) - 32) * 1.64)) let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: footerSize, - elementKind: ElementKind.footer.rawValue, + elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom ) diff --git a/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift b/Projects/Features/Falling/Src/Home/FallingHomeViewController.swift index 51b6b842..a4b269a7 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,13 +85,15 @@ final class FallingHomeViewController: TFBaseViewController { cell.bind( FallinguserCollectionViewCellModel(userDomain: item), - timerActiveTrigger, - scrollToNextObserver: timeOverSubject + timerActiveTrigger: timerActiveTrigger, + timeOverSubject: timeOverSubject, + profileDoubleTapTriggerObserver: profileDoubleTapTriggerObserver, + fallingCellButtonAction: fallingCellButtonAction ) } let footerRegistration = UICollectionView.SupplementaryRegistration - (elementKind: ElementKind.footer.rawValue) { _,_,_ in } + (elementKind: UICollectionView.elementKindSectionFooter) { _,_,_ in } dataSource = DataSource(collectionView: homeView.collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in return collectionView.dequeueConfiguredReusableCell(using: profileCellRegistration, for: indexPath, item: itemIdentifier) @@ -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..a4017c66 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,24 +256,22 @@ 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 base.profileCollectionView.transform = base.profileCollectionView.transform.rotated(by: timeState.rotateAngle) - base.pauseView.transform = base.profileCollectionView.transform.rotated(by: timeState.rotateAngle) } } 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) } } } diff --git a/Projects/Features/Falling/Src/Subviews/Cell/FallinguserCollectionViewCellModel.swift b/Projects/Features/Falling/Src/Subviews/Cell/FallinguserCollectionViewCellModel.swift index f6ab595c..0db5ab21 100644 --- a/Projects/Features/Falling/Src/Subviews/Cell/FallinguserCollectionViewCellModel.swift +++ b/Projects/Features/Falling/Src/Subviews/Cell/FallinguserCollectionViewCellModel.swift @@ -86,7 +86,7 @@ enum TimeState { 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 - 1) / 5 + return round((value / 2 - 1) / 5 * 1000) / 1000 } } diff --git a/Projects/Features/Falling/Src/Subviews/Cell/TagCollectionViewCell.swift b/Projects/Features/Falling/Src/Subviews/Cell/TagCollectionViewCell.swift index 578112e3..7d9d3e90 100644 --- a/Projects/Features/Falling/Src/Subviews/Cell/TagCollectionViewCell.swift +++ b/Projects/Features/Falling/Src/Subviews/Cell/TagCollectionViewCell.swift @@ -77,29 +77,12 @@ final class TagCollectionViewCell: UICollectionViewCell { contentView.layer.masksToBounds = true } - func bind(_ viewModel: TagItemViewModel) { - self.titleLabel.text = viewModel.title - self.emojiView.text = viewModel.emoji + func bind(_ item: EmojiType) { + self.titleLabel.text = item.name + self.emojiView.text = item.emojiCode.unicodeToEmoji() } } -struct TagItemViewModel { - let emojiCode: String - let title: String - - var emoji: String { - emojiCode.unicodeToEmoji() - } - init(_ idealType: EmojiType) { - self.title = idealType.name - self.emojiCode = idealType.emojiCode - } - - init(emojiCode: String, title: String) { - self.emojiCode = emojiCode - self.title = title - } -} #if canImport(SwiftUI) && DEBUG import SwiftUI diff --git a/Projects/Features/Falling/Src/Subviews/TagCollectionView.swift b/Projects/Features/Falling/Src/Subviews/TagCollectionView.swift deleted file mode 100644 index b8cb6011..00000000 --- a/Projects/Features/Falling/Src/Subviews/TagCollectionView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// TagCollectionView.swift -// FallingInterface -// -// Created by SeungMin on 1/11/24. -// - -// -// ProfileCollectionView.swift -// Falling -// -// Created by Kanghos on 2023/10/09. -// - -import UIKit - -import Core -import DSKit -import FallingInterface - -final class TagCollectionView: TFBaseView { - lazy var sections: [ProfileInfoSection] = [] { - didSet { - DispatchQueue.main.async { - self.collectionView.reloadData() - } - } - } - lazy var reportButton: UIButton = { - let button = UIButton() - var config = UIButton.Configuration.plain() - config.image = DSKitAsset.Image.Icons.reportFill.image.withTintColor( - DSKitAsset.Color.neutral50.color, - renderingMode: .alwaysOriginal - ) - config.imagePlacement = .all - config.baseBackgroundColor = DSKitAsset.Color.topicBackground.color - button.configuration = config - - config.automaticallyUpdateForSelection = true - return button - }() - - lazy var collectionView: UICollectionView = { - let layout = LeftAlignCollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - layout.headerReferenceSize = CGSize(width: 200, height: 50) - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.register(cellType: TagCollectionViewCell.self) - collectionView.register(cellType: ProfileIntroduceCell.self) - collectionView.register(viewType: TFCollectionReusableView.self, kind: UICollectionView.elementKindSectionHeader) - collectionView.backgroundColor = DSKitAsset.Color.neutral600.color - collectionView.isScrollEnabled = false - collectionView.dataSource = self - return collectionView - }() - - override func makeUI() { - addSubview(collectionView) - addSubview(reportButton) - collectionView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - reportButton.snp.makeConstraints { - $0.trailing.top.equalToSuperview().inset(12) - } - } -} - -extension TagCollectionView: UICollectionViewDataSource { - - func numberOfSections(in collectionView: UICollectionView) -> Int { - return self.sections.count - } - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard section < 2 else { - return 1 - } - return self.sections[section].items.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard indexPath.section < 2 else { - let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: ProfileIntroduceCell.self) - cell.bind(self.sections[indexPath.section].introduce) - return cell - } - let item = self.sections[indexPath.section].items[indexPath.item] - let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: TagCollectionViewCell.self) - cell.bind(TagItemViewModel(item)) - return cell - } - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - let header = collectionView.dequeueReusableView(for: indexPath, ofKind: kind, viewType: TFCollectionReusableView.self) - header.title = self.sections[indexPath.section].header - return header - } -} - -class LeftAlignCollectionViewFlowLayout: UICollectionViewFlowLayout { - - let cellSpacing: CGFloat = 10 - - override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - - self.minimumLineSpacing = 10.0 - sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) - - let attributes = super.layoutAttributesForElements(in: rect) - - var xPosition = sectionInset.left // Left Maring cell 추가하면 변경하고 line count에 따라 초기화 - var lineCount = -1.0 // lineCount - - // lineCount해서 전체 레이아웃을 넘어가면 line 증가 - attributes?.forEach { attribute in - if attribute.representedElementKind == UICollectionView.elementKindSectionHeader { - attribute.frame.origin.x = sectionInset.left - return - } - if attribute.indexPath.section == 2 { // 자기소개 셀 - return - } - if attribute.frame.origin.y >= lineCount { // xPosition 초기화 - xPosition = sectionInset.left - } - attribute.frame.origin.x = xPosition - xPosition += attribute.frame.width + cellSpacing - lineCount = max(attribute.frame.maxY, lineCount) - } - return attributes - } -} diff --git a/Projects/Features/Falling/Src/Subviews/UserInfoBoxView.swift b/Projects/Features/Falling/Src/Subviews/UserInfoBoxView.swift index 4891b120..88736fe4 100644 --- a/Projects/Features/Falling/Src/Subviews/UserInfoBoxView.swift +++ b/Projects/Features/Falling/Src/Subviews/UserInfoBoxView.swift @@ -11,27 +11,18 @@ import DSKit import FallingInterface final class UserInfoBoxView: TFBaseView { - lazy var tagCollectionView: TagCollectionView = { - let tagCollection = TagCollectionView() - tagCollection.layer.cornerRadius = 20 - tagCollection.clipsToBounds = true - tagCollection.collectionView.backgroundColor = DSKitAsset.Color.DimColor.default.color - tagCollection.isHidden = true - return tagCollection - }() - lazy var pageControl: UIPageControl = { let pageControl = UIPageControl() - pageControl.numberOfPages = 3 - pageControl.currentPageIndicatorTintColor = DSKitAsset.Color.neutral50.color - pageControl.tintColor = DSKitAsset.Color.neutral300.color + pageControl.pageIndicatorTintColor = DSKitAsset.Color.neutral50.color + pageControl.currentPageIndicatorTintColor = DSKitAsset.Color.neutral300.color return pageControl }() private lazy var titleLabel: UILabel = { let label = UILabel() - label.text = "최지인" + label.textColor = DSKitAsset.Color.neutral50.color label.font = UIFont.thtEx4Sb + label.setTextWithLineHeight(text: "닉네임", lineHeight: 29) return label }() @@ -45,20 +36,21 @@ final class UserInfoBoxView: TFBaseView { let label = UILabel() label.textColor = DSKitAsset.Color.neutral50.color label.font = UIFont.thtP2M + label.setTextWithLineHeight(text: "주소", lineHeight: 17) return label }() - private lazy var hStackView: UIStackView = { + private lazy var adressStackView: UIStackView = { let stackView = UIStackView() - stackView.addArrangedSubviews([pinImageView, addressLabel]) stackView.axis = .horizontal + stackView.spacing = 4 return stackView }() - private lazy var vStackView: UIStackView = { + private lazy var labelStackView: UIStackView = { let stackView = UIStackView() - stackView.addArrangedSubviews([titleLabel, hStackView]) stackView.axis = .vertical + stackView.spacing = 6 stackView.alignment = .leading return stackView }() @@ -74,65 +66,61 @@ final class UserInfoBoxView: TFBaseView { private lazy var buttonStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal - stackView.spacing = 10 + stackView.spacing = 20 + stackView.alignment = .leading return stackView }() override func makeUI() { - addSubviews([tagCollectionView, - vStackView, buttonStackView, pageControl]) - - [infoButton, spacerView, refuseButton, likeButton].forEach { subView in - buttonStackView.addArrangedSubview(subView) - subView.snp.makeConstraints { - $0.size.equalTo(80) - } + adressStackView.addArrangedSubviews([pinImageView, addressLabel]) + pinImageView.snp.makeConstraints { + $0.height.equalTo(18) + $0.width.equalTo(16) } - spacerView.snp.remakeConstraints { - $0.height.equalTo(80) - $0.width.equalTo(90).priority(.low) + labelStackView.addArrangedSubviews([titleLabel, adressStackView]) + + addSubviews([labelStackView, buttonStackView, pageControl]) + + labelStackView.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() } - tagCollectionView.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(12) - $0.height.equalTo(300).priority(.low) - $0.bottom.equalTo(vStackView.snp.top).offset(-10) + buttonStackView.snp.makeConstraints { + $0.top.equalTo(labelStackView.snp.bottom).offset(14) + $0.leading.trailing.equalToSuperview() } - vStackView.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(12) - $0.bottom.equalTo(buttonStackView.snp.top).offset(-10) + [infoButton, spacerView, refuseButton, likeButton].forEach { subView in + buttonStackView.addArrangedSubview(subView) + subView.snp.makeConstraints { + $0.size.equalTo(58) + } } - buttonStackView.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(12) - $0.bottom.equalTo(pageControl).inset(14) + spacerView.snp.remakeConstraints { + $0.height.equalTo(58) + $0.width.equalTo(92).priority(.low) } pageControl.snp.makeConstraints { - $0.width.equalTo(38) + $0.top.equalTo(buttonStackView.snp.bottom).offset(14) + $0.centerX.equalToSuperview() $0.height.equalTo(6) - $0.center.equalToSuperview() - $0.bottom.equalToSuperview().inset(12) + $0.width.greaterThanOrEqualTo(38) } } func bind(_ viewModel: FallingUser) { self.titleLabel.text = viewModel.username + ", \(viewModel.age)" self.addressLabel.text = viewModel.address - self.tagCollectionView.sections = [ - ProfileInfoSection(header: "이상형", items: viewModel.idealTypeResponseList), - ProfileInfoSection(header: "흥미", items: viewModel.interestResponses), - ProfileInfoSection(header: "자기소개", introduce: viewModel.introduction) - ] } } #if DEBUG import SwiftUI -struct CarouselViewRepresentable: UIViewRepresentable { +struct UserInfoBoxViewRepresentable: UIViewRepresentable { typealias UIViewType = UserInfoBoxView func makeUIView(context: Context) -> UIViewType { @@ -143,10 +131,10 @@ struct CarouselViewRepresentable: UIViewRepresentable { } } -struct CarouselViewPreview: PreviewProvider { +struct UserInfoBoxViewPreview: PreviewProvider { static var previews: some View { Group { - CarouselViewRepresentable() + UserInfoBoxViewRepresentable() .frame(width: UIScreen.main.bounds.width, height: 600) } .previewLayout(.sizeThatFits) diff --git a/Projects/Features/Falling/Src/Subviews/UserInfoCollectionView.swift b/Projects/Features/Falling/Src/Subviews/UserInfoCollectionView.swift new file mode 100644 index 00000000..3c1d3a23 --- /dev/null +++ b/Projects/Features/Falling/Src/Subviews/UserInfoCollectionView.swift @@ -0,0 +1,152 @@ +// +// UserInfoCollectionView.swift +// Falling +// +// Created by Kanghos on 2023/10/09. +// + +import UIKit + +import Core +import DSKit +import FallingInterface +import Domain + +final class UserInfoCollectionView: TFBaseView { + private var dataSource: DataSource! + + lazy var reportButton: UIButton = { + let button = UIButton() + button.setImage(DSKitAsset.Image.Icons.reportFill.image, for: .normal) + return button + }() + + lazy var collectionView: UICollectionView = { + let layout = UICollectionViewCompositionalLayout.horizontalTagLayout(withEstimatedHeight: 46) + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = DSKitAsset.Color.neutral600.color + collectionView.isScrollEnabled = false + collectionView.contentInset = UIEdgeInsets.init(top: 12, left: 12, bottom: 6, right: 12) + return collectionView + }() + + override func makeUI() { + addSubview(collectionView) + addSubview(reportButton) + + collectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + reportButton.snp.makeConstraints { + $0.trailing.top.equalToSuperview().inset(12) + } + + setDataSource() + } + + func bind(_ item: FallingUser) { + let ideals = item.idealTypeResponseList.map { FallingUserInfoItem.ideal($0) } + let interests = item.interestResponses.map { FallingUserInfoItem.interest($0) } + let introductions = [FallingUserInfoItem.introduction(item.introduction)] + + var snapshot = Snapshot() + snapshot.appendSections([.ideal, .interest, .introduction]) + snapshot.appendItems(ideals, toSection: .ideal) + snapshot.appendItems(interests, toSection: .interest) + snapshot.appendItems(introductions, toSection: .introduction) + dataSource.apply(snapshot) + } +} + +// MARK: DiffableDataSource + +extension UserInfoCollectionView { + typealias ModelType = FallingUserInfoItem + typealias SectionType = FallingUserInfoSection + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + func setDataSource() { + let headerRegistration = UICollectionView.SupplementaryRegistration + (elementKind: UICollectionView.elementKindSectionHeader) { + supplementaryView, elementKind, indexPath in + guard let sectionType = FallingUserInfoSection(rawValue: indexPath.section) else { return } + supplementaryView.titleLabel.text = sectionType.title + } + + let tagCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in + cell.bind(item) + } + + let introductionCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in + cell.bind(item) + } + + dataSource = DataSource(collectionView: self.collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .ideal(let item), .interest(let item): + return collectionView.dequeueConfiguredReusableCell(using: tagCellRegistration, for: indexPath, item: item) + case .introduction(let item): + return collectionView.dequeueConfiguredReusableCell(using: introductionCellRegistration, for: indexPath, item: item) + } + }) + + dataSource.supplementaryViewProvider = { (view, kind, index) in + return self.collectionView.dequeueConfiguredReusableSupplementary( + using: headerRegistration, + for: index + ) + } + } +} + +extension UICollectionViewCompositionalLayout { + static func horizontalTagLayout(withEstimatedHeight estimatedHeight: CGFloat = 46) -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout(section: .horizontalTagSection(withEstimatedHeight: estimatedHeight)) + } +} + +extension NSCollectionLayoutSection { + static func horizontalTagSection(withEstimatedHeight estimatedHeight: CGFloat = 46) -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize( + widthDimension: .estimated(40), + heightDimension: .estimated(estimatedHeight) + ) + let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) + + let layoutGroupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(estimatedHeight) + ) + + let layoutGroup = NSCollectionLayoutGroup.horizontal( + layoutSize: layoutGroupSize, + subitems: [layoutItem] + ) + layoutGroup.interItemSpacing = .fixed(6) // 아이템 간격 + + let sectionHeaderSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(17)) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: sectionHeaderSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + + let section = NSCollectionLayoutSection(group: layoutGroup) + // 섹션 간격 + section.contentInsets = NSDirectionalEdgeInsets( + top: 4, + leading: 0, + bottom: 6, + trailing: 0 + ) + section.boundarySupplementaryItems = [sectionHeader] + section.interGroupSpacing = 6 // 행 간격 + return section + } +} diff --git a/Projects/Features/Falling/Src/Subviews/UserInfoHeaderView.swift b/Projects/Features/Falling/Src/Subviews/UserInfoHeaderView.swift new file mode 100644 index 00000000..322e6f6b --- /dev/null +++ b/Projects/Features/Falling/Src/Subviews/UserInfoHeaderView.swift @@ -0,0 +1,37 @@ +// +// UserInfoHeaderView.swift +// Falling +// +// Created by SeungMin on 3/12/24. +// + +import UIKit +import DSKit + +final class UserInfoHeaderView: UICollectionReusableView { + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.thtP2Sb + label.textAlignment = .center + label.textColor = DSKitAsset.Color.neutral50.color + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { + $0.leading.top.equalToSuperview() + } + } +}