diff --git a/CardsCollectionViewHorizontalLayout.swift b/CardsCollectionViewHorizontalLayout.swift new file mode 100644 index 0000000..def49d2 --- /dev/null +++ b/CardsCollectionViewHorizontalLayout.swift @@ -0,0 +1,140 @@ +// +// CardsCollectionViewHorizontalLayout.swift +// CardsLayout +// +// Created by Filipp Fediakov on 03.01.18. +// Copyright © 2018 filletofish. All rights reserved. +// + +import UIKit + + +protocol CardsCollectionViewLayout { + var itemSize: CGSize { get set } + var spacing: CGPoint { get set } + var maximumVisibleItems: Int { get set } +} + + +open class CardsCollectionViewHorizontalLayout: UICollectionViewLayout, CardsCollectionViewLayout { + + // MARK: Layout configuration + + public var itemSize: CGSize = CGSize(width: 200, height: 300) { + didSet { + invalidateLayout() + } + } + + public var spacing: CGPoint = CGPoint(x: 10.0, y: 10.0) { + didSet { + invalidateLayout() + } + } + + public var maximumVisibleItems: Int = 4 { + didSet { + invalidateLayout() + } + } + + // MARK: - UICollectionViewLayout + + override open var collectionView: UICollectionView { + return super.collectionView! + } + + override open var collectionViewContentSize: CGSize { + let itemsCount = CGFloat(collectionView.numberOfItems(inSection: 0)) + return CGSize(width: collectionView.bounds.width * itemsCount, + height: collectionView.bounds.height) + } + + override open func prepare() { + super.prepare() + assert(collectionView.numberOfSections == 1, "Multiple sections aren't supported!") + } + + override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let totalItemsCount = collectionView.numberOfItems(inSection: 0) + let minVisibleIndex = max(Int(collectionView.contentOffset.x) / Int(collectionView.bounds.width), 0) + let maxVisibleIndex = min(minVisibleIndex + maximumVisibleItems, totalItemsCount) + let visibleIndices = minVisibleIndex.. UICollectionViewLayoutAttributes? { + let minVisibleIndex = Int(collectionView.contentOffset.x) / Int(collectionView.bounds.width) + let deltaOffset = CGFloat(Int(collectionView.contentOffset.x) % Int(collectionView.bounds.width)) + let percentageDeltaOffset = CGFloat(deltaOffset) / collectionView.bounds.width + let visibleIndex = indexPath.row - minVisibleIndex + + let attributes = UICollectionViewLayoutAttributes(forCellWith:indexPath) + attributes.size = itemSize + let midY = self.collectionView.bounds.midY + let midX = self.collectionView.bounds.midX + attributes.center = CGPoint(x: midX + spacing.x * CGFloat(visibleIndex), + y: midY + spacing.y * CGFloat(visibleIndex)) + attributes.zIndex = maximumVisibleItems - visibleIndex + attributes.transform = scaleTransform(forVisibleIndex: visibleIndex, + percentageOffset: percentageDeltaOffset) + + + switch visibleIndex { + case 0: + attributes.center.x -= deltaOffset + attributes.isHidden = false + break + case 1.. Bool { + return true + } +} + + +extension CardsCollectionViewLayout { + private func scale(at index: Int) -> CGFloat { + let translatedCoefficient = CGFloat(index) - CGFloat(self.maximumVisibleItems) / 2 + return CGFloat(pow(0.95, translatedCoefficient)) + } + + func scaleTransform(forVisibleIndex visibleIndex: Int, percentageOffset: CGFloat) -> CGAffineTransform { + guard visibleIndex != 0 else { + let scaleAtZeroIndex = scale(at: visibleIndex) + return CGAffineTransform(scaleX: scaleAtZeroIndex, y: scaleAtZeroIndex) + } + + guard visibleIndex < maximumVisibleItems else { + return CGAffineTransform.identity + } + + var rawScale = scale(at: visibleIndex) + let previousScale = scale(at: visibleIndex - 1) + let delta = (previousScale - rawScale) * percentageOffset + rawScale += delta + return CGAffineTransform(scaleX: rawScale, y: rawScale) + } +} diff --git a/CardsCollectionViewLayout.swift b/CardsCollectionViewLayout.swift deleted file mode 100644 index 82da436..0000000 --- a/CardsCollectionViewLayout.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// CardsCollectionViewLayout.swift -// CardsExample -// -// Created by Filipp Fediakov on 18.08.17. -// Copyright © 2017 filletofish. All rights reserved. -// - -import UIKit - -open class CardsCollectionViewLayout: UICollectionViewLayout { - - // MARK: - Layout configuration - - public var itemSize: CGSize = CGSize(width: 200, height: 300) { - didSet{ - invalidateLayout() - } - } - - public var spacing: CGFloat = 10.0 { - didSet{ - invalidateLayout() - } - } - - public var maximumVisibleItems: Int = 4 { - didSet{ - invalidateLayout() - } - } - - // MARK: UICollectionViewLayout - - override open var collectionView: UICollectionView { - return super.collectionView! - } - - override open var collectionViewContentSize: CGSize { - let itemsCount = CGFloat(collectionView.numberOfItems(inSection: 0)) - return CGSize(width: collectionView.bounds.width * itemsCount, - height: collectionView.bounds.height) - } - - override open func prepare() { - super.prepare() - assert(collectionView.numberOfSections == 1, "Multiple sections aren't supported!") - } - - override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - let totalItemsCount = collectionView.numberOfItems(inSection: 0) - - let minVisibleIndex = max(Int(collectionView.contentOffset.x) / Int(collectionView.bounds.width), 0) - let maxVisibleIndex = min(minVisibleIndex + maximumVisibleItems, totalItemsCount) - - let contentCenterX = collectionView.contentOffset.x + (collectionView.bounds.width / 2.0) - - let deltaOffset = Int(collectionView.contentOffset.x) % Int(collectionView.bounds.width) - - let percentageDeltaOffset = CGFloat(deltaOffset) / collectionView.bounds.width - - let visibleIndices = minVisibleIndex.. UICollectionViewLayoutAttributes? { - let contentCenterX = collectionView.contentOffset.x + (collectionView.bounds.width / 2.0) - let minVisibleIndex = Int(collectionView.contentOffset.x) / Int(collectionView.bounds.width) - let deltaOffset = Int(collectionView.contentOffset.x) % Int(collectionView.bounds.width) - let percentageDeltaOffset = CGFloat(deltaOffset) / collectionView.bounds.width - return computeLayoutAttributesForItem(indexPath: indexPath, - minVisibleIndex: minVisibleIndex, - contentCenterX: contentCenterX, - deltaOffset: CGFloat(deltaOffset), - percentageDeltaOffset: percentageDeltaOffset) - } - - override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - return true - } -} - - -// MARK: - Layout computations - -fileprivate extension CardsCollectionViewLayout { - - private func scale(at index: Int) -> CGFloat { - let translatedCoefficient = CGFloat(index) - CGFloat(self.maximumVisibleItems) / 2 - return CGFloat(pow(0.95, translatedCoefficient)) - } - - private func transform(atCurrentVisibleIndex visibleIndex: Int, percentageOffset: CGFloat) -> CGAffineTransform { - var rawScale = visibleIndex < maximumVisibleItems ? scale(at: visibleIndex) : 1.0 - - if visibleIndex != 0 { - let previousScale = scale(at: visibleIndex - 1) - let delta = (previousScale - rawScale) * percentageOffset - rawScale += delta - } - return CGAffineTransform(scaleX: rawScale, y: rawScale) - } - - fileprivate func computeLayoutAttributesForItem(indexPath: IndexPath, - minVisibleIndex: Int, - contentCenterX: CGFloat, - deltaOffset: CGFloat, - percentageDeltaOffset: CGFloat) -> UICollectionViewLayoutAttributes { - let attributes = UICollectionViewLayoutAttributes(forCellWith:indexPath) - let visibleIndex = indexPath.row - minVisibleIndex - attributes.size = itemSize - let midY = self.collectionView.bounds.midY - attributes.center = CGPoint(x: contentCenterX + spacing * CGFloat(visibleIndex), - y: midY + spacing * CGFloat(visibleIndex)) - attributes.zIndex = maximumVisibleItems - visibleIndex - - attributes.transform = transform(atCurrentVisibleIndex: visibleIndex, - percentageOffset: percentageDeltaOffset) - switch visibleIndex { - case 0: - attributes.center.x -= deltaOffset - break - case 1.. [UICollectionViewLayoutAttributes]? { + let totalItemsCount = collectionView.numberOfItems(inSection: 0) + let minVisibleIndex = max(Int(collectionView.contentOffset.y) / Int(collectionView.bounds.height), 0) + let maxVisibleIndex = min(minVisibleIndex + maximumVisibleItems, totalItemsCount) + let visibleIndices = minVisibleIndex.. UICollectionViewLayoutAttributes? { + let minVisibleIndex = Int(collectionView.contentOffset.y) / Int(collectionView.bounds.height) + let deltaOffset = CGFloat(Int(collectionView.contentOffset.y) % Int(collectionView.bounds.height)) + let percentageDeltaOffset = CGFloat(deltaOffset) / collectionView.bounds.height + let visibleIndex = indexPath.row - minVisibleIndex + + let attributes = UICollectionViewLayoutAttributes(forCellWith:indexPath) + attributes.size = itemSize + let midY = self.collectionView.bounds.midY + let midX = self.collectionView.bounds.midX + attributes.center = CGPoint(x: midX + spacing.x * CGFloat(visibleIndex), + y: midY + spacing.y * CGFloat(visibleIndex)) + attributes.zIndex = maximumVisibleItems - visibleIndex + attributes.transform = scaleTransform(forVisibleIndex: visibleIndex, + percentageOffset: percentageDeltaOffset) + + + switch visibleIndex { + case 0: + attributes.center.y -= deltaOffset + attributes.isHidden = false + break + case 1.. Bool { + return true + } +} diff --git a/CardsExample/CardsExample.xcodeproj/project.pbxproj b/CardsExample/CardsExample.xcodeproj/project.pbxproj index 8d29b2e..3390de5 100644 --- a/CardsExample/CardsExample.xcodeproj/project.pbxproj +++ b/CardsExample/CardsExample.xcodeproj/project.pbxproj @@ -12,7 +12,8 @@ 365EC0BD1FABCC9200583152 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 365EC0BB1FABCC9200583152 /* Main.storyboard */; }; 365EC0BF1FABCC9200583152 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 365EC0BE1FABCC9200583152 /* Assets.xcassets */; }; 365EC0C21FABCC9200583152 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 365EC0C01FABCC9200583152 /* LaunchScreen.storyboard */; }; - 365EC0CA1FABCD1500583152 /* CardsCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365EC0C91FABCD1400583152 /* CardsCollectionViewLayout.swift */; }; + 365EC0CA1FABCD1500583152 /* CardsCollectionViewHorizontalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365EC0C91FABCD1400583152 /* CardsCollectionViewHorizontalLayout.swift */; }; + 36B5506A1FCE28EB005BBDD5 /* CardsCollectionViewVerticalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B550691FCE28EB005BBDD5 /* CardsCollectionViewVerticalLayout.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -23,7 +24,8 @@ 365EC0BE1FABCC9200583152 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 365EC0C11FABCC9200583152 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 365EC0C31FABCC9200583152 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 365EC0C91FABCD1400583152 /* CardsCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CardsCollectionViewLayout.swift; path = ../../CardsCollectionViewLayout.swift; sourceTree = ""; }; + 365EC0C91FABCD1400583152 /* CardsCollectionViewHorizontalLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CardsCollectionViewHorizontalLayout.swift; path = ../../CardsCollectionViewHorizontalLayout.swift; sourceTree = ""; }; + 36B550691FCE28EB005BBDD5 /* CardsCollectionViewVerticalLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CardsCollectionViewVerticalLayout.swift; path = ../../CardsCollectionViewVerticalLayout.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +58,8 @@ 365EC0B61FABCC9200583152 /* CardsExample */ = { isa = PBXGroup; children = ( - 365EC0C91FABCD1400583152 /* CardsCollectionViewLayout.swift */, + 36B550691FCE28EB005BBDD5 /* CardsCollectionViewVerticalLayout.swift */, + 365EC0C91FABCD1400583152 /* CardsCollectionViewHorizontalLayout.swift */, 365EC0B71FABCC9200583152 /* AppDelegate.swift */, 365EC0B91FABCC9200583152 /* ViewController.swift */, 365EC0BB1FABCC9200583152 /* Main.storyboard */, @@ -140,7 +143,8 @@ buildActionMask = 2147483647; files = ( 365EC0BA1FABCC9200583152 /* ViewController.swift in Sources */, - 365EC0CA1FABCD1500583152 /* CardsCollectionViewLayout.swift in Sources */, + 365EC0CA1FABCD1500583152 /* CardsCollectionViewHorizontalLayout.swift in Sources */, + 36B5506A1FCE28EB005BBDD5 /* CardsCollectionViewVerticalLayout.swift in Sources */, 365EC0B81FABCC9200583152 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -278,12 +282,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8CAP6SYJTB; INFOPLIST_FILE = CardsExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = filletofish.CardsExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -293,12 +299,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 8CAP6SYJTB; INFOPLIST_FILE = CardsExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = filletofish.CardsExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/CardsExample/CardsExample/ViewController.swift b/CardsExample/CardsExample/ViewController.swift index 429e6b2..5f8c3a0 100644 --- a/CardsExample/CardsExample/ViewController.swift +++ b/CardsExample/CardsExample/ViewController.swift @@ -14,11 +14,12 @@ class ViewController: UIViewController, UICollectionViewDataSource, UICollection override func viewDidLoad() { super.viewDidLoad() - collectionView.collectionViewLayout = CardsCollectionViewLayout() + collectionView.collectionViewLayout = CardsCollectionViewHorizontalLayout() collectionView.dataSource = self collectionView.delegate = self collectionView.isPagingEnabled = true collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false } var colors: [UIColor] = [ diff --git a/CardsLayout.podspec b/CardsLayout.podspec index 46580b2..5e83a78 100644 --- a/CardsLayout.podspec +++ b/CardsLayout.podspec @@ -88,7 +88,7 @@ Pod::Spec.new do |s| # Not including the public_header_files will make all headers public. # - s.source_files = "CardsCollectionViewLayout.swift" + s.source_files = "CardsCollectionViewHorizontalLayout.swift", "CardsCollectionViewVerticalLayout.swift" s.exclude_files = "CardsExample", "CardsOldSource" # s.public_header_files = "Classes/**/*.h"