Skip to content

Commit

Permalink
✨ feat: HomeViewController 캐러셀 레이아웃 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
loinsir committed Nov 16, 2023
1 parent a7c51ad commit 6d1b847
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 51 deletions.
26 changes: 23 additions & 3 deletions iOS/Layover/Layover.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
194552282B0479B600299768 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194552272B0479B600299768 /* BaseViewController.swift */; };
1945522A2B04883800299768 /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194552292B04883800299768 /* UIView+.swift */; };
194552312B04DA1A00299768 /* LOCircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194552302B04DA1A00299768 /* LOCircleButton.swift */; };
194552392B05230E00299768 /* HomeCarouselCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194552382B05230E00299768 /* HomeCarouselCollectionViewCell.swift */; };
1945523B2B05258200299768 /* HomeConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1945523A2B05258200299768 /* HomeConfigurator.swift */; };
1945523D2B05338B00299768 /* ZoomAndSnapFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1945523C2B05338B00299768 /* ZoomAndSnapFlowLayout.swift */; };
19C7AFCE2B02410F003B35F2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFCD2B02410F003B35F2 /* AuthManager.swift */; };
19C7AFD62B02584D003B35F2 /* KeychainStored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFD52B02584D003B35F2 /* KeychainStored.swift */; };
FC2511A02B045C0A004717BC /* SignUpInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC25119F2B045C0A004717BC /* SignUpInteractor.swift */; };
Expand Down Expand Up @@ -101,6 +104,9 @@
194552272B0479B600299768 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = "<group>"; };
194552292B04883800299768 /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = "<group>"; };
194552302B04DA1A00299768 /* LOCircleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOCircleButton.swift; sourceTree = "<group>"; };
194552382B05230E00299768 /* HomeCarouselCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCarouselCollectionViewCell.swift; sourceTree = "<group>"; };
1945523A2B05258200299768 /* HomeConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeConfigurator.swift; sourceTree = "<group>"; };
1945523C2B05338B00299768 /* ZoomAndSnapFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomAndSnapFlowLayout.swift; sourceTree = "<group>"; };
19C7AFCD2B02410F003B35F2 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
19C7AFD52B02584D003B35F2 /* KeychainStored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStored.swift; sourceTree = "<group>"; };
FC25119F2B045C0A004717BC /* SignUpInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpInteractor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -192,16 +198,27 @@
1945521A2B0478A100299768 /* Home */ = {
isa = PBXGroup;
children = (
194552322B04FF2900299768 /* Cell */,
1945521E2B0478B400299768 /* HomeModels.swift */,
1945521F2B0478B400299768 /* HomeViewController.swift */,
194552202B0478B400299768 /* HomeInteractor.swift */,
1945521B2B0478B400299768 /* HomePresenter.swift */,
1945521C2B0478B400299768 /* HomeWorker.swift */,
1945521D2B0478B400299768 /* HomeRouter.swift */,
1945523A2B05258200299768 /* HomeConfigurator.swift */,
);
path = Home;
sourceTree = "<group>";
};
194552322B04FF2900299768 /* Cell */ = {
isa = PBXGroup;
children = (
194552382B05230E00299768 /* HomeCarouselCollectionViewCell.swift */,
1945523C2B05338B00299768 /* ZoomAndSnapFlowLayout.swift */,
);
path = Cell;
sourceTree = "<group>";
};
19C7AFCF2B02441C003B35F2 /* Common */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -344,7 +361,7 @@
isa = PBXGroup;
children = (
1945520E2B03AEA400299768 /* Configurator.swift */,
FCEE0FFB2B03AFAA00195BBE /* SingUpScene */,
FCEE0FFB2B03AFAA00195BBE /* SignUpScene */,
194552032B038FC400299768 /* Tabbar */,
194551EB2B037F1E00299768 /* Login */,
1945521A2B0478A100299768 /* Home */,
Expand Down Expand Up @@ -385,7 +402,7 @@
path = Resources;
sourceTree = "<group>";
};
FCEE0FFB2B03AFAA00195BBE /* SingUpScene */ = {
FCEE0FFB2B03AFAA00195BBE /* SignUpScene */ = {
isa = PBXGroup;
children = (
FCEE0FF92B03AF8400195BBE /* SignUpViewController.swift */,
Expand All @@ -394,7 +411,7 @@
FC2511A32B045D6C004717BC /* SignUpModels.swift */,
FC2511A52B049020004717BC /* SignUpConfigurator.swift */,
);
path = SingUpScene;
path = SignUpScene;
sourceTree = "<group>";
};
/* End PBXGroup section */
Expand Down Expand Up @@ -549,10 +566,13 @@
194552282B0479B600299768 /* BaseViewController.swift in Sources */,
194552212B0478B400299768 /* HomePresenter.swift in Sources */,
1945520D2B0399E500299768 /* MainTabBarViewController.swift in Sources */,
1945523D2B05338B00299768 /* ZoomAndSnapFlowLayout.swift in Sources */,
1945523B2B05258200299768 /* HomeConfigurator.swift in Sources */,
194551F62B037F2D00299768 /* LoginViewController.swift in Sources */,
FC7E458E2AFF7462004F155A /* DummyService.swift in Sources */,
FCEE0FF22B036B6000195BBE /* LOButton.swift in Sources */,
FC2511A62B049020004717BC /* SignUpConfigurator.swift in Sources */,
194552392B05230E00299768 /* HomeCarouselCollectionViewCell.swift in Sources */,
194552132B03AFFC00299768 /* DummyViewController.swift in Sources */,
194551F22B037F2D00299768 /* LoginPresenter.swift in Sources */,
194552242B0478B400299768 /* HomeModels.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions iOS/Layover/Layover/Extensions/UIView+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import UIKit

extension UIView {

static var identifier: String {
return String(describing: self)
}

func addSubviews(_ views: UIView...) {
views.forEach { addSubview($0) }
}
Expand Down
3 changes: 2 additions & 1 deletion iOS/Layover/Layover/Scenes/BaseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ class BaseViewController: UIViewController {

// MARK: - Methods

/// Call when ViewDidLoad
func setConstraints() {
// Set AutoLayout
}

/// Call when ViewDidLoad After setConstraints()
func setUI() {
// Set UI
view.backgroundColor = UIColor.background
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// HomeCarouselCollectionViewCell.swift
// Layover
//
// Created by 김인환 on 11/16/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import UIKit

final class HomeCarouselCollectionViewCell: UICollectionViewCell {

// MARK: - Properties

// MARK: - UI Components

// MARK: - Object lifecycle

override init(frame: CGRect) {
super.init(frame: frame)
setup()
setConstraints()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
setConstraints()
}

// MARK: - Setup

private func setup() {
backgroundColor = .white
}

private func setConstraints() {

}

// MARK: - Methods
}
114 changes: 114 additions & 0 deletions iOS/Layover/Layover/Scenes/Home/Cell/ZoomAndSnapFlowLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// ZoomAndSnapFlowLayout.swift
// Layover
//
// Created by 김인환 on 11/16/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import UIKit

final class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {

// MARK: Properties

private let activeDistance: CGFloat = 100
private let zoomFactor: CGFloat

// MARK: Initializer

init(zoomFactor: CGFloat = 1.0) {
self.zoomFactor = zoomFactor
super.init()
}

required init?(coder: NSCoder) {
self.zoomFactor = 1.0
super.init(coder: coder)
}

// MARK: Methods

override func prepare() {
guard let collectionView else {
super.prepare()
return
}

let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)?.compactMap { $0.copy() as? UICollectionViewLayoutAttributes } ?? []
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { $0.frame.intersects(visibleRect) }

// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var xDiff: CGFloat = 0

for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += xDiff

if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
xDiff += widthAddition * direction

if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}

// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)

return rectAttributes
}

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView else { return .zero }

// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }

var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2

for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}

return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds)
guard let invalidationContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context }
invalidationContext.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
31 changes: 31 additions & 0 deletions iOS/Layover/Layover/Scenes/Home/HomeConfigurator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// HomeConfigurator.swift
// Layover
//
// Created by 김인환 on 11/16/23.
// Copyright © 2023 CodeBomber. All rights reserved.
//

import UIKit

final class HomeConfigurator: Configurator {

static let shared = HomeConfigurator()

private init() { }

func configure(_ viewController: HomeViewController) {
let router = HomeRouter()
router.viewController = viewController

let presenter = HomePresenter()
presenter.viewController = viewController

let interactor = HomeInteractor()
interactor.presenter = presenter
interactor.worker = HomeWorker()

viewController.router = router
viewController.interactor = interactor
}
}
1 change: 0 additions & 1 deletion iOS/Layover/Layover/Scenes/Home/HomeInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class HomeInteractor: HomeBusinessLogic, HomeDataStore {
// <#Analytics Worker Instance#>.trackAnalytics(event: request.event)

let response = Models.TrackAnalytics.Response()
// presenter?.presentTrackAnalytics(with: response)
}

// MARK: - Use Case - Home
Expand Down
Loading

0 comments on commit 6d1b847

Please sign in to comment.