diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..05f1487 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + # Trigger the workflow on push or pull request, only for the master branch + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + SwiftLint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: SwiftLint Action + uses: norio-nomura/action-swiftlint@3.2.1 + + Test: + runs-on: macOS-latest + steps: + - uses: actions/checkout@v1 + - name: List available Xcode versions + run: ls /Applications | grep Xcode + - name: Select Xcode + run: sudo xcode-select -switch /Applications/Xcode_14.1.app && /usr/bin/xcodebuild -version + - name: Run unit tests + run: xcodebuild test -scheme CollectionViewPagingLayout -project CollectionViewPagingLayout.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.1' | xcpretty && exit ${PIPESTATUS[0]} + + diff --git a/.swiftlint.yml b/.swiftlint.yml index a8c67ad..9221a3b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -38,6 +38,9 @@ excluded: - Package.swift - Pods - PagingLayoutSamples/SampleProject.bundle/SampleProject + - Samples/Pods + - Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject + - CollectionViewPagingLayoutTests # adjusting rules @@ -58,12 +61,6 @@ file_header: \/\/ Copyright © \d{4} Amir Khorsandi. All rights reserved\. \/\/ -type_name: - min_length: 3 - max_length: - warning: 50 - error: 65 - excluded: id # excluded via string reporter: "xcode" cyclomatic_complexity: diff --git a/CollectionViewPagingLayout.podspec b/CollectionViewPagingLayout.podspec index 3097b06..f081bb6 100644 --- a/CollectionViewPagingLayout.podspec +++ b/CollectionViewPagingLayout.podspec @@ -1,21 +1,23 @@ - Pod::Spec.new do |s| - s.name = "CollectionViewPagingLayout" - s.version = "0.3.0" - s.summary = "Simple layout for making paging effects with UICollectionView." - - s.description = <<-DESC - A custom UICollectionViewLayout for making paging effects with custom transforms + s.name = "CollectionViewPagingLayout" + s.version = "1.1.0" + s.summary = "A simple but highly customizable layout for UICollectionView and SwiftUI." + + s.description = <<-DESC + A simple but highly customizable UICollectionViewLayout for UICollectionView. + Simple SwiftUI views that let you make page-view effects. DESC - s.homepage = "https://github.com/amirdew/CollectionViewPagingLayout" - s.license = { :type => "MIT", :file => "LICENSE" } - s.author = { "Amir Khorsandi" => "khorsandi@me.com" } - s.source = { :git => "https://github.com/amirdew/CollectionViewPagingLayout.git", :tag => "#{s.version}" } + s.homepage = "https://github.com/amirdew/CollectionViewPagingLayout" + s.license = { :type => "MIT", :file => "LICENSE" } + s.author = { "Amir Khorsandi" => "khorsandi@me.com" } + s.source = { :git => "https://github.com/amirdew/CollectionViewPagingLayout.git", :tag => "#{s.version}" } + s.source_files = ["Lib/**/*.swift"] - s.swift_versions = ["5.1"] - s.ios.deployment_target = "8.0" + s.swift_versions = ["5.5"] - s.source_files = "Lib/**/*.swift" + s.ios.deployment_target = "13.0" + s.frameworks = "UIKit" + s.weak_frameworks = "SwiftUI", "Combine" end diff --git a/CollectionViewPagingLayout.xcodeproj/project.pbxproj b/CollectionViewPagingLayout.xcodeproj/project.pbxproj index cce7571..10010f4 100644 --- a/CollectionViewPagingLayout.xcodeproj/project.pbxproj +++ b/CollectionViewPagingLayout.xcodeproj/project.pbxproj @@ -7,6 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 291EC0E32610B32500C65A34 /* PagingCollectionViewControllerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0DB2610B32500C65A34 /* PagingCollectionViewControllerBuilder.swift */; }; + 291EC0E42610B32500C65A34 /* StackPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0DC2610B32500C65A34 /* StackPageView.swift */; }; + 291EC0E52610B32500C65A34 /* ScalePageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0DD2610B32500C65A34 /* ScalePageView.swift */; }; + 291EC0E62610B32500C65A34 /* TransformPageViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0DE2610B32500C65A34 /* TransformPageViewProtocol.swift */; }; + 291EC0E72610B32500C65A34 /* PagingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0DF2610B32500C65A34 /* PagingCollectionViewCell.swift */; }; + 291EC0E82610B32500C65A34 /* TransformPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0E02610B32500C65A34 /* TransformPageView.swift */; }; + 291EC0E92610B32500C65A34 /* PagingCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0E12610B32500C65A34 /* PagingCollectionViewController.swift */; }; + 291EC0EA2610B32500C65A34 /* SnapshotPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291EC0E22610B32500C65A34 /* SnapshotPageView.swift */; }; + 291FDEC6262327FD00AD1C14 /* CollectionViewPagingLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291FDEC5262327FD00AD1C14 /* CollectionViewPagingLayoutTests.swift */; }; + 291FDEC8262327FD00AD1C14 /* CollectionViewPagingLayout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 298DBBF62441C94900341D8E /* CollectionViewPagingLayout.framework */; }; + 2967EBA226230A320035540A /* PagePadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2967EBA126230A320035540A /* PagePadding.swift */; }; + 2967EBA526230A570035540A /* PagingCollectionViewModifierData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2967EBA426230A570035540A /* PagingCollectionViewModifierData.swift */; }; 298DBBFB2441C94900341D8E /* CollectionViewPagingLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 298DBBF92441C94900341D8E /* CollectionViewPagingLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 298DBC242441C98E00341D8E /* BlurEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298DBC022441C98E00341D8E /* BlurEffectView.swift */; }; 298DBC252441C98E00341D8E /* SnapshotTransformViewOptions.PiecesValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298DBC042441C98E00341D8E /* SnapshotTransformViewOptions.PiecesValue.swift */; }; @@ -27,9 +39,37 @@ 298DBC342441C98E00341D8E /* StackTransformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298DBC1A2441C98E00341D8E /* StackTransformView.swift */; }; 298DBC352441C98E00341D8E /* StackTransformViewOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298DBC1B2441C98E00341D8E /* StackTransformViewOptions.swift */; }; 298DBC382441C98E00341D8E /* TransformableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298DBC232441C98E00341D8E /* TransformableView.swift */; }; + 29CD47C626235E6F00376CC8 /* SnapshotTransformViewOptions+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CD47C526235E6F00376CC8 /* SnapshotTransformViewOptions+Layout.swift */; }; + 29CD47CA26235E7800376CC8 /* StackTransformViewOptions+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CD47C926235E7800376CC8 /* StackTransformViewOptions+Layout.swift */; }; + 29CD47CE26235E8A00376CC8 /* ScaleTransformViewOptions+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CD47CD26235E8A00376CC8 /* ScaleTransformViewOptions+Layout.swift */; }; + 29DF4686261DF863007E0FA4 /* ViewAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DF4684261DF863007E0FA4 /* ViewAnimator.swift */; }; + 29DF4687261DF863007E0FA4 /* CollectionViewPagingLayout.ZPositionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DF4685261DF863007E0FA4 /* CollectionViewPagingLayout.ZPositionHandler.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 291FDEC9262327FD00AD1C14 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 298DBBED2441C94900341D8E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 298DBBF52441C94900341D8E; + remoteInfo = CollectionViewPagingLayout; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 291EC0DB2610B32500C65A34 /* PagingCollectionViewControllerBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingCollectionViewControllerBuilder.swift; sourceTree = ""; }; + 291EC0DC2610B32500C65A34 /* StackPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackPageView.swift; sourceTree = ""; }; + 291EC0DD2610B32500C65A34 /* ScalePageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalePageView.swift; sourceTree = ""; }; + 291EC0DE2610B32500C65A34 /* TransformPageViewProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformPageViewProtocol.swift; sourceTree = ""; }; + 291EC0DF2610B32500C65A34 /* PagingCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingCollectionViewCell.swift; sourceTree = ""; }; + 291EC0E02610B32500C65A34 /* TransformPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformPageView.swift; sourceTree = ""; }; + 291EC0E12610B32500C65A34 /* PagingCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingCollectionViewController.swift; sourceTree = ""; }; + 291EC0E22610B32500C65A34 /* SnapshotPageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotPageView.swift; sourceTree = ""; }; + 291FDEC3262327FD00AD1C14 /* CollectionViewPagingLayoutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CollectionViewPagingLayoutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 291FDEC5262327FD00AD1C14 /* CollectionViewPagingLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewPagingLayoutTests.swift; sourceTree = ""; }; + 291FDEC7262327FD00AD1C14 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2967EBA126230A320035540A /* PagePadding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagePadding.swift; sourceTree = ""; }; + 2967EBA426230A570035540A /* PagingCollectionViewModifierData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PagingCollectionViewModifierData.swift; sourceTree = ""; }; 298DBBF62441C94900341D8E /* CollectionViewPagingLayout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CollectionViewPagingLayout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 298DBBF92441C94900341D8E /* CollectionViewPagingLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CollectionViewPagingLayout.h; sourceTree = ""; }; 298DBBFA2441C94900341D8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -52,9 +92,22 @@ 298DBC1A2441C98E00341D8E /* StackTransformView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTransformView.swift; sourceTree = ""; }; 298DBC1B2441C98E00341D8E /* StackTransformViewOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTransformViewOptions.swift; sourceTree = ""; }; 298DBC232441C98E00341D8E /* TransformableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformableView.swift; sourceTree = ""; }; + 29CD47C526235E6F00376CC8 /* SnapshotTransformViewOptions+Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SnapshotTransformViewOptions+Layout.swift"; sourceTree = ""; }; + 29CD47C926235E7800376CC8 /* StackTransformViewOptions+Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StackTransformViewOptions+Layout.swift"; sourceTree = ""; }; + 29CD47CD26235E8A00376CC8 /* ScaleTransformViewOptions+Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ScaleTransformViewOptions+Layout.swift"; sourceTree = ""; }; + 29DF4684261DF863007E0FA4 /* ViewAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewAnimator.swift; sourceTree = ""; }; + 29DF4685261DF863007E0FA4 /* CollectionViewPagingLayout.ZPositionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewPagingLayout.ZPositionHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 291FDEC0262327FD00AD1C14 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 291FDEC8262327FD00AD1C14 /* CollectionViewPagingLayout.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 298DBBF32441C94900341D8E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -65,10 +118,37 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 291EC0DA2610B32500C65A34 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 291EC0E22610B32500C65A34 /* SnapshotPageView.swift */, + 2967EBA126230A320035540A /* PagePadding.swift */, + 291EC0DB2610B32500C65A34 /* PagingCollectionViewControllerBuilder.swift */, + 291EC0DC2610B32500C65A34 /* StackPageView.swift */, + 291EC0DD2610B32500C65A34 /* ScalePageView.swift */, + 2967EBA426230A570035540A /* PagingCollectionViewModifierData.swift */, + 291EC0DE2610B32500C65A34 /* TransformPageViewProtocol.swift */, + 291EC0DF2610B32500C65A34 /* PagingCollectionViewCell.swift */, + 291EC0E02610B32500C65A34 /* TransformPageView.swift */, + 291EC0E12610B32500C65A34 /* PagingCollectionViewController.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 291FDEC4262327FD00AD1C14 /* CollectionViewPagingLayoutTests */ = { + isa = PBXGroup; + children = ( + 291FDEC5262327FD00AD1C14 /* CollectionViewPagingLayoutTests.swift */, + 291FDEC7262327FD00AD1C14 /* Info.plist */, + ); + path = CollectionViewPagingLayoutTests; + sourceTree = ""; + }; 298DBBEC2441C94900341D8E = { isa = PBXGroup; children = ( 298DBBF82441C94900341D8E /* CollectionViewPagingLayout */, + 291FDEC4262327FD00AD1C14 /* CollectionViewPagingLayoutTests */, 298DBBF72441C94900341D8E /* Products */, ); sourceTree = ""; @@ -77,6 +157,7 @@ isa = PBXGroup; children = ( 298DBBF62441C94900341D8E /* CollectionViewPagingLayout.framework */, + 291FDEC3262327FD00AD1C14 /* CollectionViewPagingLayoutTests.xctest */, ); name = Products; sourceTree = ""; @@ -95,14 +176,16 @@ isa = PBXGroup; children = ( 298DBC022441C98E00341D8E /* BlurEffectView.swift */, - 298DBC032441C98E00341D8E /* Snapshot */, + 298DBC142441C98E00341D8E /* CollectionViewPagingLayout.swift */, + 298DBC232441C98E00341D8E /* TransformableView.swift */, 298DBC092441C98E00341D8E /* TransformCurve.swift */, + 29DF4685261DF863007E0FA4 /* CollectionViewPagingLayout.ZPositionHandler.swift */, + 29DF4684261DF863007E0FA4 /* ViewAnimator.swift */, 298DBC0A2441C98E00341D8E /* Scale */, - 298DBC0F2441C98E00341D8E /* Utilities */, - 298DBC142441C98E00341D8E /* CollectionViewPagingLayout.swift */, - 298DBC152441C98E00341D8E /* Sources */, + 298DBC032441C98E00341D8E /* Snapshot */, 298DBC192441C98E00341D8E /* Stack */, - 298DBC232441C98E00341D8E /* TransformableView.swift */, + 291EC0DA2610B32500C65A34 /* SwiftUI */, + 298DBC0F2441C98E00341D8E /* Utilities */, ); path = Lib; sourceTree = SOURCE_ROOT; @@ -115,6 +198,7 @@ 298DBC062441C98E00341D8E /* SnapshotTransformViewOptions.PiecePosition.swift */, 298DBC072441C98E00341D8E /* SnapshotContainerView.swift */, 298DBC082441C98E00341D8E /* SnapshotTransformViewOptions.swift */, + 29CD47C526235E6F00376CC8 /* SnapshotTransformViewOptions+Layout.swift */, ); path = Snapshot; sourceTree = ""; @@ -126,6 +210,7 @@ 298DBC0C2441C98E00341D8E /* ScaleTransformView.swift */, 298DBC0D2441C98E00341D8E /* ScaleTransformViewOptions.Rotation3dOptions.swift */, 298DBC0E2441C98E00341D8E /* ScaleTransformViewOptions.Translation3dOptions.swift */, + 29CD47CD26235E8A00376CC8 /* ScaleTransformViewOptions+Layout.swift */, ); path = Scale; sourceTree = ""; @@ -141,42 +226,12 @@ path = Utilities; sourceTree = ""; }; - 298DBC152441C98E00341D8E /* Sources */ = { - isa = PBXGroup; - children = ( - 298DBC162441C98E00341D8E /* Tests */, - 298DBC182441C98E00341D8E /* Stack */, - ); - path = Sources; - sourceTree = ""; - }; - 298DBC162441C98E00341D8E /* Tests */ = { - isa = PBXGroup; - children = ( - 298DBC172441C98E00341D8E /* LibTests */, - ); - path = Tests; - sourceTree = ""; - }; - 298DBC172441C98E00341D8E /* LibTests */ = { - isa = PBXGroup; - children = ( - ); - path = LibTests; - sourceTree = ""; - }; - 298DBC182441C98E00341D8E /* Stack */ = { - isa = PBXGroup; - children = ( - ); - path = Stack; - sourceTree = ""; - }; 298DBC192441C98E00341D8E /* Stack */ = { isa = PBXGroup; children = ( 298DBC1A2441C98E00341D8E /* StackTransformView.swift */, 298DBC1B2441C98E00341D8E /* StackTransformViewOptions.swift */, + 29CD47C926235E7800376CC8 /* StackTransformViewOptions+Layout.swift */, ); path = Stack; sourceTree = ""; @@ -195,15 +250,33 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 291FDEC2262327FD00AD1C14 /* CollectionViewPagingLayoutTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 291FDECD262327FD00AD1C14 /* Build configuration list for PBXNativeTarget "CollectionViewPagingLayoutTests" */; + buildPhases = ( + 291FDEBF262327FD00AD1C14 /* Sources */, + 291FDEC0262327FD00AD1C14 /* Frameworks */, + 291FDEC1262327FD00AD1C14 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 291FDECA262327FD00AD1C14 /* PBXTargetDependency */, + ); + name = CollectionViewPagingLayoutTests; + productName = CollectionViewPagingLayoutTests; + productReference = 291FDEC3262327FD00AD1C14 /* CollectionViewPagingLayoutTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 298DBBF52441C94900341D8E /* CollectionViewPagingLayout */ = { isa = PBXNativeTarget; buildConfigurationList = 298DBBFE2441C94900341D8E /* Build configuration list for PBXNativeTarget "CollectionViewPagingLayout" */; buildPhases = ( + 2967003224420B4100A1F508 /* Swiftlint */, 298DBBF12441C94900341D8E /* Headers */, 298DBBF22441C94900341D8E /* Sources */, 298DBBF32441C94900341D8E /* Frameworks */, 298DBBF42441C94900341D8E /* Resources */, - 2967003224420B4100A1F508 /* Swiftlint */, ); buildRules = ( ); @@ -220,9 +293,13 @@ 298DBBED2441C94900341D8E /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1130; + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1330; ORGANIZATIONNAME = Amir; TargetAttributes = { + 291FDEC2262327FD00AD1C14 = { + CreatedOnToolsVersion = 12.4; + }; 298DBBF52441C94900341D8E = { CreatedOnToolsVersion = 11.3.1; }; @@ -242,11 +319,19 @@ projectRoot = ""; targets = ( 298DBBF52441C94900341D8E /* CollectionViewPagingLayout */, + 291FDEC2262327FD00AD1C14 /* CollectionViewPagingLayoutTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 291FDEC1262327FD00AD1C14 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 298DBBF42441C94900341D8E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -278,35 +363,104 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 291FDEBF262327FD00AD1C14 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 291FDEC6262327FD00AD1C14 /* CollectionViewPagingLayoutTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 298DBBF22441C94900341D8E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 29CD47C626235E6F00376CC8 /* SnapshotTransformViewOptions+Layout.swift in Sources */, + 291EC0E52610B32500C65A34 /* ScalePageView.swift in Sources */, + 291EC0E62610B32500C65A34 /* TransformPageViewProtocol.swift in Sources */, + 29CD47CA26235E7800376CC8 /* StackTransformViewOptions+Layout.swift in Sources */, + 2967EBA226230A320035540A /* PagePadding.swift in Sources */, 298DBC2D2441C98E00341D8E /* ScaleTransformViewOptions.Rotation3dOptions.swift in Sources */, 298DBC262441C98E00341D8E /* SnapshotTransformView.swift in Sources */, + 291EC0EA2610B32500C65A34 /* SnapshotPageView.swift in Sources */, 298DBC252441C98E00341D8E /* SnapshotTransformViewOptions.PiecesValue.swift in Sources */, 298DBC292441C98E00341D8E /* SnapshotTransformViewOptions.swift in Sources */, 298DBC342441C98E00341D8E /* StackTransformView.swift in Sources */, 298DBC352441C98E00341D8E /* StackTransformViewOptions.swift in Sources */, + 29DF4687261DF863007E0FA4 /* CollectionViewPagingLayout.ZPositionHandler.swift in Sources */, 298DBC242441C98E00341D8E /* BlurEffectView.swift in Sources */, 298DBC302441C98E00341D8E /* Multipliable.swift in Sources */, + 291EC0E92610B32500C65A34 /* PagingCollectionViewController.swift in Sources */, 298DBC382441C98E00341D8E /* TransformableView.swift in Sources */, + 291EC0E82610B32500C65A34 /* TransformPageView.swift in Sources */, 298DBC312441C98E00341D8E /* CGFloat+Interpolate.swift in Sources */, + 29DF4686261DF863007E0FA4 /* ViewAnimator.swift in Sources */, 298DBC2C2441C98E00341D8E /* ScaleTransformView.swift in Sources */, 298DBC2F2441C98E00341D8E /* CGFloat+Range.swift in Sources */, + 291EC0E72610B32500C65A34 /* PagingCollectionViewCell.swift in Sources */, + 29CD47CE26235E8A00376CC8 /* ScaleTransformViewOptions+Layout.swift in Sources */, 298DBC2A2441C98E00341D8E /* TransformCurve.swift in Sources */, + 2967EBA526230A570035540A /* PagingCollectionViewModifierData.swift in Sources */, 298DBC2B2441C98E00341D8E /* ScaleTransformViewOptions.swift in Sources */, 298DBC282441C98E00341D8E /* SnapshotContainerView.swift in Sources */, + 291EC0E42610B32500C65A34 /* StackPageView.swift in Sources */, 298DBC332441C98E00341D8E /* CollectionViewPagingLayout.swift in Sources */, 298DBC2E2441C98E00341D8E /* ScaleTransformViewOptions.Translation3dOptions.swift in Sources */, 298DBC272441C98E00341D8E /* SnapshotTransformViewOptions.PiecePosition.swift in Sources */, + 291EC0E32610B32500C65A34 /* PagingCollectionViewControllerBuilder.swift in Sources */, 298DBC322441C98E00341D8E /* UIView+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 291FDECA262327FD00AD1C14 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 298DBBF52441C94900341D8E /* CollectionViewPagingLayout */; + targetProxy = 291FDEC9262327FD00AD1C14 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 291FDECB262327FD00AD1C14 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4J5W7CJ2ZV; + INFOPLIST_FILE = CollectionViewPagingLayoutTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.amir.CollectionViewPagingLayoutTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 291FDECC262327FD00AD1C14 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 4J5W7CJ2ZV; + INFOPLIST_FILE = CollectionViewPagingLayoutTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = app.amir.CollectionViewPagingLayoutTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 298DBBFC2441C94900341D8E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -333,6 +487,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -358,7 +513,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -396,6 +551,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -415,7 +571,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -438,14 +594,17 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CollectionViewPagingLayout/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = amir.app.CollectionViewPagingLayout; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -462,14 +621,17 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CollectionViewPagingLayout/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = amir.app.CollectionViewPagingLayout; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -478,6 +640,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 291FDECD262327FD00AD1C14 /* Build configuration list for PBXNativeTarget "CollectionViewPagingLayoutTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 291FDECB262327FD00AD1C14 /* Debug */, + 291FDECC262327FD00AD1C14 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 298DBBF02441C94900341D8E /* Build configuration list for PBXProject "CollectionViewPagingLayout" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/CollectionViewPagingLayout.xcodeproj/xcshareddata/xcschemes/CollectionViewPagingLayout.xcscheme b/CollectionViewPagingLayout.xcodeproj/xcshareddata/xcschemes/CollectionViewPagingLayout.xcscheme index 3cd80cd..250ff93 100644 --- a/CollectionViewPagingLayout.xcodeproj/xcshareddata/xcschemes/CollectionViewPagingLayout.xcscheme +++ b/CollectionViewPagingLayout.xcodeproj/xcshareddata/xcschemes/CollectionViewPagingLayout.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CollectionViewPagingLayout/Info.plist b/CollectionViewPagingLayout/Info.plist index 9bcb244..c0701c6 100644 --- a/CollectionViewPagingLayout/Info.plist +++ b/CollectionViewPagingLayout/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/CollectionViewPagingLayoutTests/CollectionViewPagingLayoutTests.swift b/CollectionViewPagingLayoutTests/CollectionViewPagingLayoutTests.swift new file mode 100644 index 0000000..d8f4c24 --- /dev/null +++ b/CollectionViewPagingLayoutTests/CollectionViewPagingLayoutTests.swift @@ -0,0 +1,20 @@ +// +// CollectionViewPagingLayoutTests.swift +// CollectionViewPagingLayoutTests +// +// Created by Amir on 11/04/2021. +// Copyright © 2021 Amir. All rights reserved. +// + +import XCTest +@testable import CollectionViewPagingLayout + +class CollectionViewPagingLayoutTests: XCTestCase { + + func testAssignToCollectionView() throws { + let layout = CollectionViewPagingLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + XCTAssert(collectionView.collectionViewLayout == layout) + } + +} diff --git a/CollectionViewPagingLayoutTests/Info.plist b/CollectionViewPagingLayoutTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/CollectionViewPagingLayoutTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/HOW_TO_USE_SWIFTUI.md b/HOW_TO_USE_SWIFTUI.md new file mode 100644 index 0000000..a436d9e --- /dev/null +++ b/HOW_TO_USE_SWIFTUI.md @@ -0,0 +1,255 @@ + +## How to use with `SwiftUI` + +*If you'd like to follow code instead of docs go to [Sample Code](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_SWIFTUI.md#sample-code)* + + +- First, make sure you imported the framework +```swift +import CollectionViewPagingLayout +``` + +### Prepared Page views + +There are three prepared page views to make it easier to use this framework. +- `ScalePageView` (orange previews) +- `SnapshotPageView` (green previews) +- `StackPageView` (blue previews) + +These views are highly customizable, you can make tons of different effects using them. +Here is a simple example for `StackPageView`: + +```swift + var body: some View { + StackPageView(items) { item in + // Build your view here + ZStack { + Rectangle().fill(Color.orange) + Text("\(item.number)") + } + } + .pagePadding(.absolute(50)) + .options(.init( + scaleFactor: 0.12, + maxStackSize: 4, + popAngle: .pi / 10, + popOffsetRatio: .init(width: -1.45, height: 0.3), + stackPosition: .init(x: 1.5, y: 0) + )) + } +``` +There is an "options" property for each of these page views where you can customize the effect, check the struct to find out what each parameter does. + + +### TransformPageView + +You can build your custom effects using this view. + +```swift + var body: some View { + TransformPageView(items) { item, progress in + // Build your view here + ZStack { + Rectangle().fill(Color.orange) + Text("\(item.number)") + } + // Apply transform here + .transformEffect(.init(translationX: progress * 200, y: 0)) + } + // The padding around each page + // you can use `.fractionalWidth` and + // `.fractionalHeight` too + .pagePadding( + vertical: .absolute(100), + horizontal: .absolute(80) + ) + } + +``` +As you see above, you get a `progress` value. Use that to apply any effect you want. + +> `progress` is a float value that represents the current position of the page view. +> When it's `0` that means the page position is exactly in the center of TransformPageView. +> The value could be negative or positive and that represents the distance to the center of your TransformPageView. +> for instance `1` means the distance between the center of the page and the center of TransformPageView is equal to your TransformPageView width. + +in above example we `translationX: progress * 200` meaning current page will be in the center (translationX = 0), the page before current page will be -200 pixels behind and so on ... + +### Selection + +You can pass a binding value when you initialize your page view along with `items`: +`selection: Binding` + +This is a two-way binding selection, which means you can change it to animate the page to the selected page, and the value gets changed when the user navigates between pages. + + +## Modifiers +### - `.pagePadding` +By default, the content view size will be equal to the parent view, you can use this modifier to add padding around pages. Padding can be `absolute`, `fractionalHeight` or `fractionalWidth`: +```swift + enum Padding { + /// Creates a padding with an absolute point value. + case absolute(CGFloat) + + /// Creates a padding that is computed as a fraction of the height of the container view. + case fractionalHeight(CGFloat) + + /// Creates a padding that is computed as a fraction of the width of the container view. + case fractionalWidth(CGFloat) + } +``` + +You can use one of these or combine them: +```swift +pagePadding(_ padding: ) +pagePadding(vertical: horizontal: ) +pagePadding(top: left: bottom: right:) +``` + + +### - `.numberOfVisibleItems` +By default, all of the pages get load immediately. +If you have lots of pages and you want to have lazy loading, use this modifier. + +```swift +numberOfVisibleItems(_ count: Int) +``` + + +### - `.zPosition` +Use this modifier to provide zPosition(zIndex) for each page. +The default value: `Int(-abs(round(progress)))` + +```swift +zPosition(_ zPosition: (progress: CGFloat) -> Int) +``` + +### - `.onTapPage` +*Note: in most cases you want to use `selection: Binding` instead of this modifer.* + +As the name says this modifier can be used to handle tap on page. +This is equivalent for `collectionView(_ collectionView:, didSelectItemAt indexPath:)` + +```swift +onTapPage(_ closure: (ValueType.ID) -> Void) +``` + +### - `.animator` +Use this modifer to define your animator. +[`DefaultViewAnimator`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/ViewAnimator.swift#L29) allows you to specify animation duration and curve function. + +You can implement your custom animator too!. + +```swift +animator(_ animator: ViewAnimator) +``` + +### - `.scrollToSelectedPage` +if this is enabled page view automaticly scrolls to the selected page. +default value: `true` + +```swift +scrollToSelectedPage(_ goToSelectedPage: Bool) +``` + +### - `.scrollDirection` +Use to specify the scroll direction `.vertical` or `.horizontal` + + +### - `.collectionView` +Using this modifier, you can set value for the underlying UICollectionView's properties + +for instance, if you want to show vertical scroll indicators: +`.collectionView(\.showsVerticalScrollIndicator, true)` + + +```swift +collectionView(_ key: WritableKeyPath, _ value: T) +``` + + + +## Sample Code + +`ContentView` implementing a scale effect: + +```swift +import SwiftUI +import CollectionViewPagingLayout + +struct ContentView: View { + + // Replace with your data + struct Item: Identifiable { + let id: UUID = .init() + let number: Int + } + let items = Array(0..<10).map { + Item(number: $0) + } + + // Use the options to customize the layout + var options: ScaleTransformViewOptions { + .layout(.linear) + } + + var body: some View { + ScalePageView(items) { item in + // Build your view here + ZStack { + Rectangle().fill(Color.orange) + Text("\(item.number)") + } + } + .options(options) + // The padding around each page + // you can use `.fractionalWidth` and + // `.fractionalHeight` too + .pagePadding( + vertical: .absolute(100), + horizontal: .absolute(80) + ) + } + +} +``` + +`ContentView` implementing a custom effect: + + +```swift +struct ContentView: View { + + // Replace with your data + struct Item: Identifiable { + let id: UUID = .init() + let number: Int + } + let items = Array(0..<10).map { + Item(number: $0) + } + + var body: some View { + TransformPageView(items) { item, progress in + // Build your view here + ZStack { + Rectangle().fill(Color.orange) + Text("\(item.number)") + } + // Apply transform and other effects + .scaleEffect(1 - abs(progress) * 0.6) + .transformEffect(.init(translationX: progress * 200, y: 0)) + .blur(radius: abs(progress) * 20) + .opacity(1.8 - Double(abs(progress))) + } + // The padding around each page + // you can use `.fractionalWidth` and + // `.fractionalHeight` too + .pagePadding( + vertical: .absolute(100), + horizontal: .absolute(80) + ) + } + +} +``` \ No newline at end of file diff --git a/HOW_TO_USE_UIKIT.md b/HOW_TO_USE_UIKIT.md new file mode 100644 index 0000000..ac0a125 --- /dev/null +++ b/HOW_TO_USE_UIKIT.md @@ -0,0 +1,245 @@ + +## How to use with `UIKit` + +*If you'd like to follow code instead of docs go to [Sample Code](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_UIKIT.md#sample-code)* + +- First, make sure you imported the framework +```swift +import CollectionViewPagingLayout +``` +- Set up your `UICollectionView` as you always do (you need a custom class for cells) +- Set the layout for your collection view: +(in most cases you want a paging effect so enable that too) +```swift +let layout = CollectionViewPagingLayout() +collectionView.collectionViewLayout = layout +collectionView.isPagingEnabled = true // enabling paging effect +layout.numberOfVisibleItems = nil // default=nil means it's equal to number of items in CollectionView +``` + +### Prepared Transformable Protocols + +*If you'd like to build your custom effect, go to [Make custom effects](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_UIKIT.md#make-custom-effects)* + +There are some prepared transformable protocols to make it easier to use this framework. +Using them is simple. You only need to conform your `UICollectionViewCell` to the protocol. +You can use the options property to tweak it as you want. +There are three types: +- `ScaleTransformView` (orange previews) +- `SnapshotTransformView` (green previews) +- `StackTransformView` (blue previews) +These protocols are highly customizable, you can make tons of different effects using them. +Here is a simple example for `ScaleTransformView` which gives you a simple paging with scaling effect: +```swift +extension YourCell: ScaleTransformView { + var scaleOptions: ScaleTransformViewOptions { + ScaleTransformViewOptions( + minScale: 0.6, + scaleRatio: 0.4, + translationRatio: CGPoint(x: 0.66, y: 0.2), + maxTranslationRatio: CGPoint(x: 2, y: 0) + ) + } +} +``` +There is an "options" property for each of these protocols where you can customize the effect, check the struct to find out what each parameter does. +A short comment on the top of each parameter explains what that does. + +If you want to replicate the same effects that you see in previews, use the `Layout` extension file. + +`ScaleTransformView` -> [`ScaleTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions.swift) [`.Layout`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift) +`SnapshotTransformView` -> [`SnapshotTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions.swift) [`.Layout`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift) +`StackTransformView` -> [`StackTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions.swift) [`.Layout`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift) + +#### Target View +All the effects apply to one subview of your cell. that is the target view. +By default, target view is the first subview of `cell.contentView`. (Override it If that's not what you want) +The contentView size will always be equal to the UICollectionView size. So, It's important to consider the padding inside your cell. +For instance, if you want to show 20 percent of the next and the previous page, your target view width should be 60 percent of `cell.contentView`. + +Target View named as below: + +`ScaleTransformView` -> [`scalableView`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformView.swift#L18) +`SnapshotTransformView` -> [`targetView`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformView.swift#L17) +`StackTransformView` -> [`cardView`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformView.swift#L18) + + + +### Customize or Combine Prepared Transformables + +Yes, you can customize them or even combine them. +To do that, implement `TransformableView.transform` function and call the transformable function manually, like this: +```swift +extension LayoutTypeCollectionViewCell: ScaleTransformView { + + func transform(progress: CGFloat) { + applyScaleTransform(progress: progress) + // customize views here, like this: + titleLabel.alpha = 1 - abs(progress) + subtitleLabel.alpha = titleLabel.alpha + } + +} +``` +As you see, `applyScaleTransform` applies the scale transforms and right after that we change the alpha for `titleLabel` and `subtitleLabel`. +To find the public function(s) of each protocol check the definition of that. + + +## Make custom effects. + +Conform your cell class to `TransformableView` and start implementing your custom transforms. +for instance: +```swift +class YourCell: UICollectionViewCell { /*...*/ } +extension YourCell: TransformableView { + func transform(progress: CGFloat) { + // apply changes on any view of your cell + } +} +``` +As you see above, you get a `progress` value. Use that to apply any changes you want. + +> `progress` is a float value that represents the current position of your cell in the collection view. +> When it's `0` that means the current position of the cell is exactly in the center of your CollectionView. +> the value could be negative or positive and that represents the distance to the center of your CollectionView. +> for instance `1` means the distance between the center of the cell and the center of your collection view is equal to your CollectionView width. + + +you can start with a simple transform like this: +```swift +extension YourCell: TransformableView { + func transform(progress: CGFloat) { + let transform = CGAffineTransform(translationX: bounds.width/2 * progress, y: 0) + let alpha = 1 - abs(progress) + + contentView.subviews.forEach { $0.transform = transform } + contentView.alpha = alpha + } +} +``` + +## Features + +### Control current page + +You can control the current page by the following functions of `CollectionViewPagingLayout`: +- [`func setCurrentPage(Int)`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/CollectionViewPagingLayout.swift#L101) +- [`func goToNextPage()`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/CollectionViewPagingLayout.swift#L107) +- [`func goToPreviousPage()`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/CollectionViewPagingLayout.swift#L113) + +These are safe wrappers around setting the `ContentOffset` of `UICollectionview`. +You can get the current page by a public variable: `CollectionViewPagingLayout.currentPage`. +Listen to the changes using [`CollectionViewPagingLayout.delegate`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/CollectionViewPagingLayout.swift#L11): +```swift +public protocol CollectionViewPagingLayoutDelegate: class { + func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) +} +``` + +### ViewAnimator + +A custom animator that you can use to animate `ContentOffset`. + +[`DefaultViewAnimator`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/ViewAnimator.swift#L29) allows you to specify animation duration and curve function. + +You can implement your custom animator too!. + +You can specify a default animator `CollectionViewPagingLayout.defaultAnimator` which will be used for all animations unless you specify animator per animation: +`setCurrentPage(_ page: Int, animated: Bool, animator: ViewAnimator?)`, + + + +## Sample Code + +UIViewController: + +```swift +import UIKit +import CollectionViewPagingLayout + +// A simple View Controller that filled with a UICollectionView +// You can use `UICollectionViewController` too +class ViewController: UIViewController, UICollectionViewDataSource { + + var collectionView: UICollectionView! + + override func viewDidLoad() { + super.viewDidLoad() + setupCollectionView() + } + + private func setupCollectionView() { + let layout = CollectionViewPagingLayout() + collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout) + collectionView.isPagingEnabled = true + collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell") + collectionView.dataSource = self + view.addSubview(collectionView) + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + 10 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) + } + +} +``` + +UICollectionViewCell: + +```swift +class MyCell: UICollectionViewCell { + + // The card view that we apply transforms on + var card: UIView! + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setup() { + // Adjust the card view frame + // you can use Auto-layout too + let cardFrame = CGRect( + x: 80, + y: 100, + width: frame.width - 160, + height: frame.height - 200 + ) + card = UIView(frame: cardFrame) + card.backgroundColor = .systemOrange + contentView.addSubview(card) + } +} + +``` + +`MyCell` implementing a [scale effect](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_UIKIT.md#prepared-transformable-protocols): + +```swift +extension MyCell: ScaleTransformView { + var scaleOptions: ScaleTransformViewOptions { + .layout(.linear) + } +} +``` + +`MyCell` implementing a [custom effect]((https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_UIKIT.md#make-custom-effects)): + +```swift +extension MyCell: TransformableView { + func transform(progress: CGFloat) { + let alpha = 1 - abs(progress) + contentView.alpha = alpha + } +} +``` diff --git a/Lib/BlurEffectView.swift b/Lib/BlurEffectView.swift index 52aeb3f..c7ac78d 100644 --- a/Lib/BlurEffectView.swift +++ b/Lib/BlurEffectView.swift @@ -8,7 +8,6 @@ import UIKit -@available(iOS 10.0, *) public class BlurEffectView: UIVisualEffectView { // MARK: Parameters diff --git a/Lib/CollectionViewPagingLayout.ZPositionHandler.swift b/Lib/CollectionViewPagingLayout.ZPositionHandler.swift new file mode 100644 index 0000000..3773683 --- /dev/null +++ b/Lib/CollectionViewPagingLayout.ZPositionHandler.swift @@ -0,0 +1,22 @@ +// +// CollectionViewPagingLayout.ZPositionHandler.swift +// CollectionViewPagingLayout +// +// Created by Amir on 03/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation + +public extension CollectionViewPagingLayout { + enum ZPositionHandler { + /// Sets cell.layer.zPosition + case cellLayer + + /// Sets UICollectionViewLayoutAttributes.zIndex + case layoutAttribute + + /// Sets both of `cellLayer` and `layoutAttribute` + case both + } +} diff --git a/Lib/CollectionViewPagingLayout.swift b/Lib/CollectionViewPagingLayout.swift index c4e5386..f9f1b86 100644 --- a/Lib/CollectionViewPagingLayout.swift +++ b/Lib/CollectionViewPagingLayout.swift @@ -8,43 +8,56 @@ import UIKit -public protocol CollectionViewPagingLayoutDelegate: class { +public protocol CollectionViewPagingLayoutDelegate: AnyObject { /// Calls when the current page changes /// /// - Parameter layout: a reference to the layout class /// - Parameter currentPage: the new current page index func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) - - /// Calls when the user taps on the `TransformableView.selectableView` - /// to enable this functionality you need to call `configureTapOnCollectionView()` after setting the layout - /// - /// - Parameter layout: a reference to the layout class - /// - Parameter indexPath: IndexPath for the selected cell - func collectionViewPagingLayout(_ layout: CollectionViewPagingLayout, didSelectItemAt indexPath: IndexPath) } public extension CollectionViewPagingLayoutDelegate { func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) {} - func collectionViewPagingLayout(_ layout: CollectionViewPagingLayout, didSelectItemAt indexPath: IndexPath) {} } public class CollectionViewPagingLayout: UICollectionViewLayout { // MARK: Properties - + + /// Number of visible items at the same time + /// + /// nil = no limit public var numberOfVisibleItems: Int? - + + /// Constants that indicate the direction of scrolling for the layout. public var scrollDirection: UICollectionView.ScrollDirection = .horizontal + + /// See `ZPositionHandler` for details + public var zPositionHandler: ZPositionHandler = .both + + /// Set `alpha` to zero when the cell is not loaded yet by collection view, enabling this prevents showing a cell before applying + /// transforms but may cause flashing when you reload the data + public var transparentAttributeWhenCellNotLoaded: Bool = false + + /// The animator for setting `contentOffset` + /// + /// See `ViewAnimator` for details + public var defaultAnimator: ViewAnimator? + + public private(set) var isAnimating: Bool = false public weak var delegate: CollectionViewPagingLayoutDelegate? override public var collectionViewContentSize: CGSize { getContentSize() } - + + /// Current page index + /// + /// Use `setCurrentPage` to change it public private(set) var currentPage: Int = 0 { didSet { delegate?.onCurrentPageChanged(layout: self, currentPage: currentPage) @@ -70,27 +83,58 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { } private var currentPageCache: Int? - private var attributesCache: [(page: Int, attributes:UICollectionViewLayoutAttributes)]? - private var scrollToSelectedCell: Bool = false - - + private var attributesCache: [(page: Int, attributes: UICollectionViewLayoutAttributes)]? + private var boundsObservation: NSKeyValueObservation? + private var lastBounds: CGRect? + private var currentViewAnimatorCancelable: ViewAnimatorCancelable? + private var originalIsUserInteractionEnabled: Bool? + private var contentOffsetObservation: NSKeyValueObservation? + + // MARK: Public functions - public func setCurrentPage(_ page: Int, animated: Bool = true) { - safelySetCurrentPage(page, animated: animated) + public func setCurrentPage(_ page: Int, + animated: Bool = true, + animator: ViewAnimator? = nil, + completion: (() -> Void)? = nil) { + safelySetCurrentPage(page, animated: animated, animator: animator, completion: completion) } - - public func goToNextPage(animated: Bool = true) { - setCurrentPage(currentPage + 1, animated: animated) + + public func setCurrentPage(_ page: Int, + animated: Bool = true, + completion: (() -> Void)? = nil) { + safelySetCurrentPage(page, animated: animated, animator: defaultAnimator, completion: completion) } - public func goToPreviousPage(animated: Bool = true) { - setCurrentPage(currentPage - 1, animated: animated) + public func goToNextPage(animated: Bool = true, + animator: ViewAnimator? = nil, + completion: (() -> Void)? = nil) { + setCurrentPage(currentPage + 1, animated: animated, animator: animator, completion: completion) } - public func configureTapOnCollectionView(goToSelectedPage: Bool = false) { - self.scrollToSelectedCell = goToSelectedPage - addTapGestureToCollectionView() + public func goToPreviousPage(animated: Bool = true, + animator: ViewAnimator? = nil, + completion: (() -> Void)? = nil) { + setCurrentPage(currentPage - 1, animated: animated, animator: animator, completion: completion) + } + + /// Calls `invalidateLayout` wrapped in `performBatchUpdates` + /// - Parameter invalidateOffset: change offset and revert it immediately + /// this fixes the zIndex issue more: https://stackoverflow.com/questions/12659301/uicollectionview-setlayoutanimated-not-preserving-zindex + public func invalidateLayoutInBatchUpdate(invalidateOffset: Bool = false) { + DispatchQueue.main.async { [weak self] in + if invalidateOffset, + let collectionView = self?.collectionView, + self?.isAnimating == false { + let original = collectionView.contentOffset + collectionView.contentOffset = .init(x: original.x + 1, y: original.y + 1) + collectionView.contentOffset = original + } + + self?.collectionView?.performBatchUpdates({ [weak self] in + self?.invalidateLayout() + }) + } } @@ -102,7 +146,7 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { } return true } - + override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let currentScrollOffset = self.currentScrollOffset let numberOfItems = self.numberOfItems @@ -123,7 +167,7 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { let startIndex = max(0, initialStartIndex - endIndexOutOfBounds) let endIndex = min(numberOfItems, initialEndIndex + startIndexOutOfBounds) - var attributesArray: [(page: Int, attributes:UICollectionViewLayoutAttributes)] = [] + var attributesArray: [(page: Int, attributes: UICollectionViewLayoutAttributes)] = [] var section = 0 var numberOfItemsInSection = collectionView?.numberOfItems(inSection: section) ?? 0 var numberOfItemsInPrevSections = 0 @@ -150,14 +194,26 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { if cell == nil || cell is TransformableView { cellAttributes.frame = visibleRect + if cell == nil, transparentAttributeWhenCellNotLoaded { + cellAttributes.alpha = 0 + } } else { - cellAttributes.frame = CGRect(origin: CGPoint(x: pageIndex * visibleRect.width, y: 0), size: visibleRect.size) + cellAttributes.frame = CGRect(origin: CGPoint(x: pageIndex * visibleRect.width, y: 0), + size: visibleRect.size) + } + + // In some cases attribute.zIndex doesn't work so this is the work-around + if let cell = cell, [ZPositionHandler.both, .cellLayer].contains(zPositionHandler) { + cell.layer.zPosition = CGFloat(zIndex) + } + + if [ZPositionHandler.both, .layoutAttribute].contains(zPositionHandler) { + cellAttributes.zIndex = zIndex } - - cellAttributes.zIndex = zIndex attributesArray.append((page: Int(pageIndex), attributes: cellAttributes)) } attributesCache = attributesArray + addBoundsObserverIfNeeded() return attributesArray.map(\.attributes) } @@ -186,7 +242,7 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { currentPage = Int(round(offset / pageSize)) } } - if currentPage != self.currentPage { + if currentPage != self.currentPage, !isAnimating { self.currentPage = currentPage } } @@ -205,60 +261,80 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { } } - private func safelySetCurrentPage(_ page: Int, animated: Bool) { + private func safelySetCurrentPage(_ page: Int, animated: Bool, animator: ViewAnimator?, completion: (() -> Void)? = nil) { + if isAnimating { + currentViewAnimatorCancelable?.cancel() + isAnimating = false + if let isEnabled = originalIsUserInteractionEnabled { + collectionView?.isUserInteractionEnabled = isEnabled + } + } let pageSize = scrollDirection == .horizontal ? visibleRect.width : visibleRect.height let contentSize = scrollDirection == .horizontal ? collectionViewContentSize.width : collectionViewContentSize.height let maxPossibleOffset = contentSize - pageSize - var offset = pageSize * CGFloat(page) + var offset = Double(pageSize) * Double(page) offset = max(0, offset) - offset = min(offset, maxPossibleOffset) + offset = min(offset, Double(maxPossibleOffset)) let contentOffset: CGPoint = scrollDirection == .horizontal ? CGPoint(x: offset, y: 0) : CGPoint(x: 0, y: offset) - CATransaction.begin() - CATransaction.setCompletionBlock { [weak self] in - self?.invalidateLayout() + + if animated { + isAnimating = true + } + + if animated, let animator = animator { + setContentOffset(with: animator, offset: contentOffset, completion: completion) + } else { + contentOffsetObservation = collectionView?.observe(\.contentOffset, options: [.new]) { [weak self] _, _ in + if self?.collectionView?.contentOffset == contentOffset { + self?.contentOffsetObservation = nil + DispatchQueue.main.async { [weak self] in + self?.invalidateLayoutInBatchUpdate() + self?.collectionView?.setContentOffset(contentOffset, animated: false) + self?.isAnimating = false + completion?() + } + } + } + collectionView?.setContentOffset(contentOffset, animated: animated) } - collectionView?.setContentOffset(contentOffset, animated: animated) - CATransaction.commit() // this is necessary when we want to set the current page without animation - if !animated, page != currentPage, let collectionView = collectionView { - collectionView.performBatchUpdates({ - collectionView.collectionViewLayout.invalidateLayout() - }) + if !animated, page != currentPage { + invalidateLayoutInBatchUpdate() } } - - private func addTapGestureToCollectionView() { - let gesture = UITapGestureRecognizer(target: self, action: #selector(tapOnCollectionView(gesture:))) - collectionView?.addGestureRecognizer(gesture) - } - - @objc private func tapOnCollectionView(gesture: UITapGestureRecognizer) { - var items = collectionView?.visibleCells.compactMap { cell -> (cell: UICollectionViewCell, rect: CGRect, attributes: UICollectionViewLayoutAttributes, page: Int)? in - guard let indexPath = collectionView?.indexPath(for: cell), - let view = cell as? TransformableView, - let selectableView = view.selectableView, - let attributesAndPage = attributesCache?.first(where: { $0.attributes.indexPath == indexPath }) else { - return nil - } - let rect = selectableView.superview?.convert(selectableView.frame, to: collectionView) ?? .zero - return (cell: cell, rect: rect, attributes: attributesAndPage.attributes, page: attributesAndPage.page) - } ?? [] - - items.sort { $0.attributes.zIndex > $1.attributes.zIndex } - - let location = gesture.location(in: gesture.view) - var findSelected = false - for item in items { - if !findSelected, item.rect.contains(location) { - delegate?.collectionViewPagingLayout(self, didSelectItemAt: item.attributes.indexPath) - item.cell.isSelected = true - findSelected = true - if scrollToSelectedCell { - setCurrentPage(item.page, animated: true) - } + + private func setContentOffset(with animator: ViewAnimator, offset: CGPoint, completion: (() -> Void)? = nil) { + guard let start = collectionView?.contentOffset else { return } + let x = offset.x - start.x + let y = offset.y - start.y + originalIsUserInteractionEnabled = collectionView?.isUserInteractionEnabled ?? true + collectionView?.isUserInteractionEnabled = false + currentViewAnimatorCancelable = animator.animate { [weak self] progress, finished in + guard let collectionView = self?.collectionView else { return } + collectionView.contentOffset = CGPoint(x: start.x + x * CGFloat(progress), + y: start.y + y * CGFloat(progress)) + if finished { + self?.currentViewAnimatorCancelable = nil + self?.isAnimating = false + self?.collectionView?.isUserInteractionEnabled = self?.originalIsUserInteractionEnabled ?? true + self?.originalIsUserInteractionEnabled = nil + self?.collectionView?.delegate?.scrollViewDidEndScrollingAnimation?(collectionView) + self?.invalidateLayoutInBatchUpdate() + completion?() } - item.cell.isSelected = false + } + } +} + + +extension CollectionViewPagingLayout { + private func addBoundsObserverIfNeeded() { + guard boundsObservation == nil else { return } + boundsObservation = collectionView?.observe(\.bounds, options: [.old, .new, .initial, .prior]) { [weak self] collectionView, _ in + guard collectionView.bounds.size != self?.lastBounds?.size else { return } + self?.lastBounds = collectionView.bounds + self?.invalidateLayoutInBatchUpdate(invalidateOffset: true) } } } diff --git a/Lib/Scale/ScaleTransformView.swift b/Lib/Scale/ScaleTransformView.swift index 5fb44ed..9d10c22 100644 --- a/Lib/Scale/ScaleTransformView.swift +++ b/Lib/Scale/ScaleTransformView.swift @@ -18,7 +18,7 @@ public protocol ScaleTransformView: TransformableView { var scalableView: UIView { get } /// The view to apply blur effect on - var blurViewHost: UIView { get } + var scaleBlurViewHost: UIView { get } /// the main function for applying transforms func applyScaleTransform(progress: CGFloat) @@ -28,7 +28,7 @@ public protocol ScaleTransformView: TransformableView { public extension ScaleTransformView { /// The default value is the super view of `scalableView` - var blurViewHost: UIView { + var scaleBlurViewHost: UIView { scalableView.superview ?? scalableView } } @@ -82,12 +82,17 @@ public extension ScaleTransformView { } let layer = scalableView.layer layer.shadowColor = scaleOptions.shadowColor.cgColor + + let progressMultiplier = 1 - abs(progress) + let widthProgressValue = progressMultiplier * scaleOptions.shadowOffsetMax.width + let heightProgressValue = progressMultiplier * scaleOptions.shadowOffsetMax.height + let offset = CGSize( - width: max(scaleOptions.shadowOffsetMin.width, (1 - abs(progress)) * scaleOptions.shadowOffsetMax.width), - height: max(scaleOptions.shadowOffsetMin.height, (1 - abs(progress)) * scaleOptions.shadowOffsetMax.height) + width: max(scaleOptions.shadowOffsetMin.width, widthProgressValue), + height: max(scaleOptions.shadowOffsetMin.height, heightProgressValue) ) layer.shadowOffset = offset - layer.shadowRadius = max(scaleOptions.shadowRadiusMin, (1 - abs(progress)) * scaleOptions.shadowRadiusMax) + layer.shadowRadius = max(scaleOptions.shadowRadiusMin, progressMultiplier * scaleOptions.shadowRadiusMax) layer.shadowOpacity = max(scaleOptions.shadowOpacityMin, (1 - abs(Float(progress))) * scaleOptions.shadowOpacityMax) } @@ -155,19 +160,18 @@ public extension ScaleTransformView { } scalableView.layer.transform = transform } - - @available(iOS 10.0, *) + private func applyBlurEffect(progress: CGFloat) { guard scaleOptions.blurEffectRadiusRatio > 0, scaleOptions.blurEffectEnabled else { - blurViewHost.subviews.first(where: { $0 is BlurEffectView })?.removeFromSuperview() + scaleBlurViewHost.subviews.first(where: { $0 is BlurEffectView })?.removeFromSuperview() return } let blurView: BlurEffectView - if let view = blurViewHost.subviews.first(where: { $0 is BlurEffectView }) as? BlurEffectView { + if let view = scaleBlurViewHost.subviews.first(where: { $0 is BlurEffectView }) as? BlurEffectView { blurView = view } else { blurView = BlurEffectView(effect: UIBlurEffect(style: scaleOptions.blurEffectStyle)) - blurViewHost.fill(with: blurView) + scaleBlurViewHost.fill(with: blurView) } blurView.setBlurRadius(radius: abs(progress) * scaleOptions.blurEffectRadiusRatio) blurView.transform = CGAffineTransform.identity.translatedBy(x: scalableView.transform.tx, y: scalableView.transform.ty) diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+ScaleOptions.swift b/Lib/Scale/ScaleTransformViewOptions+Layout.swift similarity index 62% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+ScaleOptions.swift rename to Lib/Scale/ScaleTransformViewOptions+Layout.swift index 2d62863..6625331 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+ScaleOptions.swift +++ b/Lib/Scale/ScaleTransformViewOptions+Layout.swift @@ -1,19 +1,30 @@ // -// ShapeLayout+ScaleOptions.swift -// PagingLayoutSamples +// ScaleTransformViewOptions+Layout.swift +// CollectionViewPagingLayout // -// Created by Amir on 27/06/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. // +import UIKit import Foundation -import CollectionViewPagingLayout -extension ShapeLayout { - var scaleOptions: ScaleTransformViewOptions? { - switch self { - case .scaleBlur: - return ScaleTransformViewOptions( +public extension ScaleTransformViewOptions { + enum Layout: String, CaseIterable { + case invertedCylinder + case cylinder + case coverFlow + case rotary + case linear + case easeIn + case easeOut + case blur + } + + static func layout(_ layout: Layout) -> Self { + switch layout { + case .blur: + return Self( minScale: 0.6, scaleRatio: 0.4, translationRatio: CGPoint(x: 0.66, y: 0.2), @@ -21,8 +32,8 @@ extension ShapeLayout { blurEffectEnabled: true, blurEffectRadiusRatio: 0.2 ) - case .scaleLinear: - return ScaleTransformViewOptions( + case .linear: + return Self( minScale: 0.6, scaleRatio: 0.4, translationRatio: CGPoint(x: 0.66, y: 0.2), @@ -32,8 +43,8 @@ extension ShapeLayout { scaleCurve: .linear, translationCurve: .linear ) - case .scaleEaseIn: - return ScaleTransformViewOptions( + case .easeIn: + return Self( minScale: 0.6, scaleRatio: 0.4, translationRatio: CGPoint(x: 0.66, y: 0.2), @@ -42,8 +53,8 @@ extension ShapeLayout { scaleCurve: .easeIn, translationCurve: .linear ) - case .scaleEaseOut: - return ScaleTransformViewOptions( + case .easeOut: + return Self( minScale: 0.6, scaleRatio: 0.4, translationRatio: CGPoint(x: 0.66, y: 0.2), @@ -52,8 +63,8 @@ extension ShapeLayout { scaleCurve: .linear, translationCurve: .easeIn ) - case .scaleRotary: - return ScaleTransformViewOptions( + case .rotary: + return Self( minScale: 0, scaleRatio: 0.4, translationRatio: CGPoint(x: 0.1, y: 0.1), @@ -64,8 +75,8 @@ extension ShapeLayout { minTranslateRatios: (-3, -0.8, -0.3), maxTranslateRatios: (3, 0.8, -0.3)) ) - case .scaleCylinder: - return ScaleTransformViewOptions( + case .cylinder: + return Self( minScale: 0.55, maxScale: 0.55, scaleRatio: 0, @@ -76,8 +87,8 @@ extension ShapeLayout { rotation3d: .init(angle: .pi / 4, minAngle: -.pi, maxAngle: .pi, x: 0, y: 1, z: 0, m34: -0.000_4 - 0.8 * 0.000_2 ), translation3d: .init(translateRatios: (0, 0, 0), minTranslateRatios: (0, 0, 1.25), maxTranslateRatios: (0, 0, 1.25)) ) - case .scaleInvertedCylinder: - return ScaleTransformViewOptions( + case .invertedCylinder: + return Self( minScale: 1.2, maxScale: 1.2, scaleRatio: 0, @@ -90,8 +101,31 @@ extension ShapeLayout { minTranslateRatios: (-0.05, 0, 0.86), maxTranslateRatios: (0.05, 0, -0.86)) ) - case .scaleCoverFlow: - return ScaleTransformViewOptions( + case .coverFlow: + let defaultAngle: Double = .pi / 1.65 + let minAngle: Double = -.pi / 3 + let maxAngle: Double = .pi / 3 + let rotation3d = Rotation3dOptions( + angle: defaultAngle, + minAngle: minAngle, + maxAngle: maxAngle, + x: 0, + y: -1, + z: 0, + m34: -0.000_5 + ) + + + let translateRatios: (CGFloat, CGFloat, CGFloat) = (0.1, 0, -0.7) + let minTranslateRatios: (CGFloat, CGFloat, CGFloat) = (-0.1, 0, -3) + let maxTranslateRatios: (CGFloat, CGFloat, CGFloat) = (0.1, 0, 0) + let translation3d = Translation3dOptions( + translateRatios: translateRatios, + minTranslateRatios: minTranslateRatios, + maxTranslateRatios: maxTranslateRatios + ) + + return Self( minScale: 0.7, maxScale: 0.7, scaleRatio: 0, @@ -99,11 +133,9 @@ extension ShapeLayout { minTranslationRatio: .zero, maxTranslationRatio: .zero, shadowEnabled: true, - rotation3d: .init(angle: .pi / 1.65, minAngle: -.pi / 3, maxAngle: .pi / 3, x: 0, y: -1, z: 0, m34: -0.000_5), - translation3d: .init(translateRatios: (0.1, 0, -0.7), minTranslateRatios: (-0.1, 0, -3), maxTranslateRatios: (0.1, 0, 0)) + rotation3d: rotation3d, + translation3d: translation3d ) - default: - return nil } } } diff --git a/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift b/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift index 6264ba1..127eb3b 100644 --- a/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift +++ b/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift @@ -11,7 +11,7 @@ import UIKit public extension ScaleTransformViewOptions { struct Translation3dOptions { - + // MARK: Properties /// The translates(x,y,z) ratios @@ -39,5 +39,5 @@ public extension ScaleTransformViewOptions { self.maxTranslateRatios = maxTranslateRatios } } - + } diff --git a/Lib/Snapshot/SnapshotContainerView.swift b/Lib/Snapshot/SnapshotContainerView.swift index c4ed4b6..378461f 100644 --- a/Lib/Snapshot/SnapshotContainerView.swift +++ b/Lib/Snapshot/SnapshotContainerView.swift @@ -17,7 +17,7 @@ public class SnapshotContainerView: UIView { public let snapshotSize: CGSize public let pieceSizeRatio: CGSize - private let targetView: UIView + private weak var targetView: UIView? // MARK: Lifecycle diff --git a/Lib/Snapshot/SnapshotTransformView.swift b/Lib/Snapshot/SnapshotTransformView.swift index 7445bfa..8491414 100644 --- a/Lib/Snapshot/SnapshotTransformView.swift +++ b/Lib/Snapshot/SnapshotTransformView.swift @@ -16,15 +16,18 @@ public protocol SnapshotTransformView: TransformableView { /// The view to apply the effect on var targetView: UIView { get } - /// The identifier for snapshot, it won't make a new snapshot if + /// A unique identifier for the snapshot, a new snapshot won't be made if /// there is a cashed snapshot with the same identifier - var identifier: String { get } + var snapshotIdentifier: String { get } /// the function for getting the cached snapshot or make a new one and cache it func getSnapshot() -> SnapshotContainerView? /// the main function for applying transforms on the snapshot func applySnapshotTransform(snapshot: SnapshotContainerView, progress: CGFloat) + + /// Check if the snapshot can be reused + func canReuse(snapshot: SnapshotContainerView) -> Bool } @@ -37,9 +40,9 @@ public extension SnapshotTransformView where Self: UICollectionViewCell { } /// Default `identifier` for `UICollectionViewCell` is it's index - /// if you have the same content with different indexes (like infinite list) + /// if you have the same content with different indexes (like an infinite list) /// you should override this and provide a content-based identifier - var identifier: String { + var snapshotIdentifier: String { var collectionView: UICollectionView? var superview = self.superview while superview != nil { @@ -49,7 +52,22 @@ public extension SnapshotTransformView where Self: UICollectionViewCell { } superview = superview?.superview } - return "\(collectionView?.indexPath(for: self) ?? IndexPath())" + var identifier = "\(collectionView?.indexPath(for: self) ?? IndexPath())" + + if let scrollView = targetView as? UIScrollView { + identifier.append("\(scrollView.contentOffset)") + } + + if let scrollView = targetView.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView { + identifier.append("\(scrollView.contentOffset)") + } + + return identifier + } + + /// Default implementation only compares the size of snapshot with the view + func canReuse(snapshot: SnapshotContainerView) -> Bool { + snapshot.snapshotSize == targetView.bounds.size } } @@ -92,25 +110,46 @@ public extension SnapshotTransformView { // MARK: Private functions - + + private func hideOtherSnapshots() { + targetView.superview?.subviews.filter { $0 is SnapshotContainerView }.forEach { + guard let snapshot = $0 as? SnapshotContainerView else { return } + if snapshot.identifier != snapshotIdentifier { + snapshot.alpha = 0 + } + } + } + private func findSnapshot() -> SnapshotContainerView? { - let snapshot = targetView.superview?.subviews.first(where: { $0 is SnapshotContainerView }) as? SnapshotContainerView - if let snapshot = snapshot, - (snapshot.identifier != identifier || - snapshot.snapshotSize != targetView.bounds.size || - snapshot.pieceSizeRatio != snapshotOptions.pieceSizeRatio) - { + hideOtherSnapshots() + + let snapshot = targetView.superview?.subviews.first { + ($0 as? SnapshotContainerView)?.identifier == snapshotIdentifier + } as? SnapshotContainerView + + if let snapshot = snapshot, snapshot.pieceSizeRatio != snapshotOptions.pieceSizeRatio { snapshot.removeFromSuperview() return nil } + if let snapshot = snapshot, !canReuse(snapshot: snapshot) { + snapshot.removeFromSuperview() + return nil + } + snapshot?.alpha = 1 return snapshot } private func makeSnapshot() -> SnapshotContainerView? { - guard let view = SnapshotContainerView(targetView: targetView, pieceSizeRatio: snapshotOptions.pieceSizeRatio, identifier: identifier) else { - return nil - } - targetView.superview?.subviews.first(where: { $0 is SnapshotContainerView })?.removeFromSuperview() + targetView.superview?.subviews.first { + ($0 as? SnapshotContainerView)?.identifier == snapshotIdentifier + }? + .removeFromSuperview() + + guard let view = SnapshotContainerView(targetView: targetView, + pieceSizeRatio: snapshotOptions.pieceSizeRatio, + identifier: snapshotIdentifier) + else { return nil } + targetView.superview?.insertSubview(view, aboveSubview: targetView) targetView.equalSize(to: view) targetView.center(to: view) diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+SnapshotOptions.swift b/Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift similarity index 80% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+SnapshotOptions.swift rename to Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift index 6cea80b..1c2b8d9 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+SnapshotOptions.swift +++ b/Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift @@ -1,19 +1,30 @@ // -// ShapeLayout+SnapshotOptions.swift -// PagingLayoutSamples +// SnapshotTransformViewOptions+Layout.swift +// CollectionViewPagingLayout // -// Created by Amir on 27/06/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. // +import UIKit import Foundation -import CollectionViewPagingLayout -extension ShapeLayout { - var snapshotOptions: SnapshotTransformViewOptions? { - switch self { - case .snapshotGrid: - return SnapshotTransformViewOptions( +public extension SnapshotTransformViewOptions { + enum Layout: String, CaseIterable { + case grid + case space + case chess + case tiles + case lines + case bars + case puzzle + case fade + } + + static func layout(_ layout: Layout) -> Self { + switch layout { + case .grid: + return Self( pieceSizeRatio: .init(width: 1.0 / 4.0, height: 1.0 / 10.0), piecesCornerRadiusRatio: .static(1), piecesAlphaRatio: .static(0), @@ -22,8 +33,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 0.7, y: 0) ) - case .snapshotSpace: - return SnapshotTransformViewOptions( + case .space: + return Self( pieceSizeRatio: .init(width: 1.0 / 3.0, height: 1.0 / 4.0), piecesCornerRadiusRatio: .static(0.7), piecesAlphaRatio: .aggregated([.rowBasedMirror(0.2), .columnBasedMirror(0.4)], +), @@ -32,8 +43,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 0.7, y: 0) ) - case .snapshotChess: - return SnapshotTransformViewOptions( + case .chess: + return Self( pieceSizeRatio: .init(width: 1.0 / 5.0, height: 1.0 / 10.0), piecesCornerRadiusRatio: .static(0.5), piecesAlphaRatio: .columnBasedMirror(0.4), @@ -42,8 +53,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 0.7, y: 0) ) - case .snapshotTiles: - return SnapshotTransformViewOptions( + case .tiles: + return Self( pieceSizeRatio: .init(width: 1, height: 1.0 / 10.0), piecesCornerRadiusRatio: .static(0), piecesAlphaRatio: .static(0.4), @@ -52,8 +63,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 1, y: 0) ) - case .snapshotLines: - return SnapshotTransformViewOptions( + case .lines: + return Self( pieceSizeRatio: .init(width: 1, height: 1.0 / 16.0), piecesCornerRadiusRatio: .static(0), piecesAlphaRatio: .static(0.4), @@ -62,8 +73,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 0.8, y: 0) ) - case .snapshotBars: - return SnapshotTransformViewOptions( + case .bars: + return Self( pieceSizeRatio: .init(width: 1.0 / 10.0, height: 1), piecesCornerRadiusRatio: .static(1.2), piecesAlphaRatio: .static(0.4), @@ -72,8 +83,8 @@ extension ShapeLayout { containerScaleRatio: 0.1, containerTranslationRatio: .init(x: 1, y: 0) ) - case .snapshotPuzzle: - return SnapshotTransformViewOptions( + case .puzzle: + return Self( pieceSizeRatio: .init(width: 1.0 / 4.0, height: 1.0 / 10.0), piecesCornerRadiusRatio: .static(0), piecesAlphaRatio: .aggregated([.rowOddEven(0.2, 0), .columnOddEven(0, 0.2)], +), @@ -82,8 +93,8 @@ extension ShapeLayout { containerScaleRatio: 0.2, containerTranslationRatio: .init(x: 1, y: 0) ) - case .snapshotFade: - return SnapshotTransformViewOptions( + case .fade: + return Self( pieceSizeRatio: .init(width: 1, height: 1.0 / 10.0), piecesCornerRadiusRatio: .static(0.1), piecesAlphaRatio: .rowBased(0.1), @@ -92,9 +103,6 @@ extension ShapeLayout { containerScaleRatio: 0.7, containerTranslationRatio: .init(x: 1.9, y: 0) ) - default: - return nil } - } } diff --git a/Lib/Snapshot/SnapshotTransformViewOptions.PiecePosition.swift b/Lib/Snapshot/SnapshotTransformViewOptions.PiecePosition.swift index a28aeec..0333704 100644 --- a/Lib/Snapshot/SnapshotTransformViewOptions.PiecePosition.swift +++ b/Lib/Snapshot/SnapshotTransformViewOptions.PiecePosition.swift @@ -10,7 +10,7 @@ import UIKit public extension SnapshotTransformViewOptions { - class PiecePosition { + struct PiecePosition { // MARK: Properties diff --git a/Lib/Snapshot/SnapshotTransformViewOptions.PiecesValue.swift b/Lib/Snapshot/SnapshotTransformViewOptions.PiecesValue.swift index 47a6f3c..4310611 100644 --- a/Lib/Snapshot/SnapshotTransformViewOptions.PiecesValue.swift +++ b/Lib/Snapshot/SnapshotTransformViewOptions.PiecesValue.swift @@ -11,7 +11,7 @@ import UIKit public extension SnapshotTransformViewOptions { enum PiecesValue { - + // MARK: Cases case columnBased(Type, reversed: Bool = false) diff --git a/Lib/Stack/StackTransformView.swift b/Lib/Stack/StackTransformView.swift index 90b0eb9..aa8e4e8 100644 --- a/Lib/Stack/StackTransformView.swift +++ b/Lib/Stack/StackTransformView.swift @@ -18,7 +18,7 @@ public protocol StackTransformView: TransformableView { var cardView: UIView { get } /// The view to apply blur effect on - var blurViewHost: UIView { get } + var stackBlurViewHost: UIView { get } /// the main function for applying transforms func applyStackTransform(progress: CGFloat) @@ -28,7 +28,7 @@ public protocol StackTransformView: TransformableView { public extension StackTransformView { /// The default value is the super view of `cardView` - var blurViewHost: UIView { + var stackBlurViewHost: UIView { cardView.superview ?? cardView } @@ -187,19 +187,18 @@ public extension StackTransformView { cardView.transform = cardView.transform.rotated(by: angle) } - - @available(iOS 10.0, *) + private func applyBlurEffect(progress: CGFloat) { guard stackOptions.maxBlurEffectRadius > 0, stackOptions.blurEffectEnabled else { - blurViewHost.subviews.first(where: { $0 is BlurEffectView })?.removeFromSuperview() + stackBlurViewHost.subviews.first(where: { $0 is BlurEffectView })?.removeFromSuperview() return } let blurView: BlurEffectView - if let view = blurViewHost.subviews.first(where: { $0 is BlurEffectView }) as? BlurEffectView { + if let view = stackBlurViewHost.subviews.first(where: { $0 is BlurEffectView }) as? BlurEffectView { blurView = view } else { blurView = BlurEffectView() - blurViewHost.fill(with: blurView) + stackBlurViewHost.fill(with: blurView) } let radius = max(progress, 0).interpolate(in: .init(0, CGFloat(stackOptions.maxStackSize))) blurView.setBlurRadius(effect: UIBlurEffect(style: stackOptions.blurEffectStyle), radius: radius * stackOptions.maxBlurEffectRadius) diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+StackOptions.swift b/Lib/Stack/StackTransformViewOptions+Layout.swift similarity index 76% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+StackOptions.swift rename to Lib/Stack/StackTransformViewOptions+Layout.swift index c13502d..35a7490 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeLayout+StackOptions.swift +++ b/Lib/Stack/StackTransformViewOptions+Layout.swift @@ -1,19 +1,28 @@ // -// ShapeLayout+StackOptions.swift -// PagingLayoutSamples +// StackTransformViewOptions+Layout.swift +// CollectionViewPagingLayout // -// Created by Amir on 27/06/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. // +import UIKit import Foundation -import CollectionViewPagingLayout -extension ShapeLayout { - var stackOptions: StackTransformViewOptions? { - switch self { - case .stackTransparent: - return StackTransformViewOptions( +public extension StackTransformViewOptions { + enum Layout: String, CaseIterable { + case transparent + case perspective + case rotary + case vortex + case reverse + case blur + } + + static func layout(_ layout: Layout) -> Self { + switch layout { + case .transparent: + return Self( scaleFactor: 0.12, minScale: 0.0, maxStackSize: 4, @@ -23,8 +32,8 @@ extension ShapeLayout { popAngle: .pi / 10, popOffsetRatio: .init(width: -1.45, height: 0.3) ) - case .stackPerspective: - return StackTransformViewOptions( + case .perspective: + return Self( scaleFactor: 0.1, minScale: 0.2, maxStackSize: 6, @@ -36,8 +45,8 @@ extension ShapeLayout { popOffsetRatio: .init(width: -1.45, height: 0.3), stackPosition: CGPoint(x: 1, y: 0) ) - case .stackRotary: - return StackTransformViewOptions( + case .rotary: + return Self( scaleFactor: -0.03, minScale: 0.2, maxStackSize: 3, @@ -49,8 +58,8 @@ extension ShapeLayout { popOffsetRatio: .init(width: -1.45, height: 0.4), stackPosition: CGPoint(x: 0, y: 1) ) - case .stackVortex: - return StackTransformViewOptions( + case .vortex: + return Self( scaleFactor: -0.15, minScale: 0.2, maxScale: nil, @@ -64,8 +73,8 @@ extension ShapeLayout { popOffsetRatio: .zero, stackPosition: CGPoint(x: 0, y: 1) ) - case .stackReverse: - return StackTransformViewOptions( + case .reverse: + return Self( scaleFactor: 0.1, maxScale: nil, maxStackSize: 4, @@ -76,8 +85,8 @@ extension ShapeLayout { stackPosition: CGPoint(x: -1, y: -0.2), reverse: true ) - case .stackBlur: - return StackTransformViewOptions( + case .blur: + return Self( scaleFactor: 0.1, maxScale: nil, maxStackSize: 7, @@ -92,8 +101,6 @@ extension ShapeLayout { blurEffectEnabled: true, maxBlurEffectRadius: 0.08 ) - default: - return nil } } } diff --git a/Lib/SwiftUI/PagePadding.swift b/Lib/SwiftUI/PagePadding.swift new file mode 100644 index 0000000..0e2b6b5 --- /dev/null +++ b/Lib/SwiftUI/PagePadding.swift @@ -0,0 +1,30 @@ +// +// PagePadding.swift +// CollectionViewPagingLayout +// +// Created by Amir Khorsandi on 10/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import UIKit + +/// Provides paddings around the page +public struct PagePadding { + + let top: Padding? + let left: Padding? + let bottom: Padding? + let right: Padding? + + public enum Padding { + /// Creates a padding with an absolute point value. + case absolute(CGFloat) + + /// Creates a padding that is computed as a fraction of the height of the container view. + case fractionalHeight(CGFloat) + + /// Creates a padding that is computed as a fraction of the width of the container view. + case fractionalWidth(CGFloat) + } +} diff --git a/Lib/SwiftUI/PagingCollectionViewCell.swift b/Lib/SwiftUI/PagingCollectionViewCell.swift new file mode 100644 index 0000000..fe156ed --- /dev/null +++ b/Lib/SwiftUI/PagingCollectionViewCell.swift @@ -0,0 +1,180 @@ +// +// PagingCollectionViewCell.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI +import UIKit + +class PagingCollectionViewCell: UICollectionViewCell { + typealias Parent = PagingCollectionViewController + + // MARK: Properties + + private weak var hostingController: UIHostingController? + private var viewBuilder: ((ValueType, CGFloat) -> Content)? + private var value: ValueType! + private var index: IndexPath! + private weak var parent: Parent? + private var parentBoundsObserver: NSKeyValueObservation? + private var parentSize: CGSize? + + // MARK: Public functions + + func update(value: ValueType, index: IndexPath, parent: Parent) { + self.parent = parent + viewBuilder = parent.pageViewBuilder + self.value = value + self.index = index + if hostingController != nil { + updateView() + } else { + let viewController = UIHostingController(rootView: updateView()!) + hostingController = viewController + viewController.view.backgroundColor = .clear + + parent.addChild(viewController) + contentView.addSubview(viewController.view) + viewController.view.translatesAutoresizingMaskIntoConstraints = false + + parentBoundsObserver = parent.view + .observe(\.bounds, options: [.initial, .new, .old, .prior]) { [weak self] _, _ in + self?.updatePagePaddings() + } + + viewController.didMove(toParent: parent) + viewController.view.layoutIfNeeded() + } + } + + // MARK: Private functions + + @discardableResult private func updateView(progress: CGFloat? = nil) -> Content? { + guard let viewBuilder = viewBuilder + else { return nil } + + let view = viewBuilder(value, progress ?? 0) + hostingController?.rootView = view + hostingController?.view.layoutIfNeeded() + return view + } + + private func updatePagePaddings() { + guard let parent = parent, + let viewController = hostingController, + parent.view.bounds.size != parentSize + else { return } + + parentSize = parent.view.bounds.size + + func constraint(_ first: NSLayoutAnchor, + _ second: NSLayoutAnchor, + _ paddingKeyPath: KeyPath, + _ inside: Bool) { + let padding = parent.modifierData?.pagePadding?[keyPath: paddingKeyPath] ?? .absolute(0) + let constant: CGFloat + switch padding { + case .fractionalWidth(let fraction): + constant = parent.view.bounds.size.width * fraction + case .fractionalHeight(let fraction): + constant = parent.view.bounds.size.height * fraction + case .absolute(let absolute): + constant = absolute + } + let identifier = "pagePaddingConstraint_\(inside)_\(T.self)" + if let constraint = contentView.constraints.first(where: { $0.identifier == identifier }) ?? + viewController.view.constraints.first(where: { $0.identifier == identifier }) { + constraint.constant = constant * (inside ? 1 : -1) + } else { + let constraint = first.constraint(equalTo: second, constant: constant * (inside ? 1 : -1)) + constraint.identifier = identifier + constraint.isActive = true + } + } + + constraint(contentView.leadingAnchor, viewController.view.leadingAnchor, \.left, false) + constraint(contentView.trailingAnchor, viewController.view.trailingAnchor, \.right, true) + constraint(contentView.topAnchor, viewController.view.topAnchor, \.top, false) + constraint(contentView.bottomAnchor, viewController.view.bottomAnchor, \.bottom, true) + } +} + +extension PagingCollectionViewCell: TransformableView, + ScaleTransformView, + StackTransformView, + SnapshotTransformView { + var scalableView: UIView { + hostingController?.view ?? contentView + } + + var cardView: UIView { + hostingController?.view ?? contentView + } + + var targetView: UIView { + hostingController?.view ?? contentView + } + + var selectableView: UIView? { + scalableView + } + + var scaleOptions: ScaleTransformViewOptions { + parent?.modifierData?.scaleOptions ?? .init() + } + + var stackOptions: StackTransformViewOptions { + parent?.modifierData?.stackOptions ?? .init() + } + + var snapshotOptions: SnapshotTransformViewOptions { + parent?.modifierData?.snapshotOptions ?? .init() + } + + func transform(progress: CGFloat) { + if parent?.modifierData?.scaleOptions != nil { + applyScaleTransform(progress: progress) + } + if parent?.modifierData?.stackOptions != nil { + applyStackTransform(progress: progress) + } + if parent?.modifierData?.snapshotOptions != nil { + if let snapshot = getSnapshot() { + applySnapshotTransform(snapshot: snapshot, progress: progress) + } + } + updateView(progress: progress) + } + + func zPosition(progress: CGFloat) -> Int { + parent?.modifierData?.zPositionProvider?(progress) ?? Int(-abs(round(progress))) + } + + var snapshotIdentifier: String { + if let snapshotIdentifier = parent?.modifierData?.snapshotIdentifier { + return snapshotIdentifier(index.item, hostingController?.view) + } + + var identifier = String(describing: value.id) + + if let scrollView = targetView as? UIScrollView { + identifier.append("\(scrollView.contentOffset)") + } + + if let scrollView = targetView.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView { + identifier.append("\(scrollView.contentOffset)") + } + + return identifier + } + + func canReuse(snapshot: SnapshotContainerView) -> Bool { + if let canReuse = parent?.modifierData?.canReuseSnapshot { + return canReuse(snapshot, hostingController?.view) + } + return snapshot.snapshotSize == targetView.bounds.size + } +} diff --git a/Lib/SwiftUI/PagingCollectionViewController.swift b/Lib/SwiftUI/PagingCollectionViewController.swift new file mode 100644 index 0000000..3969f1e --- /dev/null +++ b/Lib/SwiftUI/PagingCollectionViewController.swift @@ -0,0 +1,175 @@ +// +// PagingCollectionViewController.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import UIKit +import SwiftUI + +public class PagingCollectionViewController: UIViewController, + UICollectionViewDataSource, + CollectionViewPagingLayoutDelegate, + UICollectionViewDelegate, + UIScrollViewDelegate { + + // MARK: Properties + + var modifierData: PagingCollectionViewModifierData? + var pageViewBuilder: ((ValueType, CGFloat) -> PageContent)! + var onCurrentPageChanged: ((Int) -> Void)? + + private var collectionView: UICollectionView! + private var list: [ValueType] = [] + private let layout = CollectionViewPagingLayout() + + + // MARK: UIViewController + + public override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + setupCollectionView() + } + + + // MARK: Public functions + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + list.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: PagingCollectionViewCell = collectionView.dequeueReusableCellClass(for: indexPath) + cell.update(value: list[indexPath.row], index: indexPath, parent: self) + return cell + } + + public func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) { + onCurrentPageChanged?(currentPage) + } + + + // MARK: Internal functions + + func update(list: [ValueType], currentIndex: Int?) { + var needsUpdate = false + + if let self = self as? PagingCollectionViewControllerEquatableList { + needsUpdate = !self.isListSame(as: list) + } else { + let oldIds = self.list.map(\.id) + needsUpdate = list.map(\.id) != oldIds + } + self.list = list + if needsUpdate { + collectionView?.reloadData() + layout.invalidateLayoutInBatchUpdate(invalidateOffset: true) + } + let index = currentIndex ?? layout.currentPage + if index < list.count { + guard index != layout.currentPage else { return } + view.isUserInteractionEnabled = false + layout.setCurrentPage(index) { [weak view] in + view?.isUserInteractionEnabled = true + } + } else { + layout.invalidateLayoutInBatchUpdate() + } + } + + // MARK: UICollectionViewDelegate + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + modifierData?.onTapPage?(indexPath.row) + + if modifierData?.goToSelectedPage ?? true { + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.setCurrentPage(indexPath.row) + } + } + + + // MARK: Private functions + + private func setupCollectionView() { + collectionView = UICollectionView( + frame: view.frame, + collectionViewLayout: layout + ) + layout.delegate = self + collectionView.backgroundColor = .clear + collectionView.registerClass(PagingCollectionViewCell.self) + collectionView.dataSource = self + view.fill(with: collectionView) + layout.numberOfVisibleItems = modifierData?.numberOfVisibleItems + layout.scrollDirection = modifierData?.scrollDirection ?? layout.scrollDirection + layout.defaultAnimator = modifierData?.animator + layout.transparentAttributeWhenCellNotLoaded = modifierData?.transparentAttributeWhenCellNotLoaded ?? layout.transparentAttributeWhenCellNotLoaded + collectionView.delegate = self + + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.isPagingEnabled = true + modifierData?.collectionViewProperties.forEach { property in + if let keyPath: WritableKeyPath = property.getKey(), + let value: Bool = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: UIView = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: UIColor = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: UIScrollView.ContentInsetAdjustmentBehavior = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: UIEdgeInsets = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: CGFloat = property.getValue() { + collectionView[keyPath: keyPath] = value + } + if let keyPath: WritableKeyPath = property.getKey(), + let value: CGSize = property.getValue() { + collectionView[keyPath: keyPath] = value + } + } + } + + +} + +private protocol PagingCollectionViewControllerEquatableList { + func isListSame(as list: [T]) -> Bool +} + +extension PagingCollectionViewController: PagingCollectionViewControllerEquatableList where ValueType: Equatable { + func isListSame(as list: [T]) -> Bool { + self.list == (list as? [ValueType]) + } +} + +private extension UICollectionView { + func registerClass(_ cellType: T.Type, reuseIdentifier: String = T.reuseIdentifier) { + register(cellType, forCellWithReuseIdentifier: reuseIdentifier) + } + + func dequeueReusableCellClass(for indexPath: IndexPath, type: T.Type? = nil, reuseIdentifier: String = T.reuseIdentifier) -> T { + (dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? T)! + } +} + + +private extension UICollectionViewCell { + static var reuseIdentifier: String { + String(describing: self) + } +} diff --git a/Lib/SwiftUI/PagingCollectionViewControllerBuilder.swift b/Lib/SwiftUI/PagingCollectionViewControllerBuilder.swift new file mode 100644 index 0000000..ed15a56 --- /dev/null +++ b/Lib/SwiftUI/PagingCollectionViewControllerBuilder.swift @@ -0,0 +1,78 @@ +// +// PagingCollectionViewControllerBuilder.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI + +public class PagingCollectionViewControllerBuilder { + + public typealias ViewController = PagingCollectionViewController + + // MARK: Properties + + let data: [ValueType] + let pageViewBuilder: (ValueType, CGFloat) -> PageContent + let selection: Binding? + + var modifierData: PagingCollectionViewModifierData = .init() + + weak var viewController: ViewController? + + + // MARK: Lifecycle + + public init( + data: [ValueType], + pageViewBuilder: @escaping (ValueType, CGFloat) -> PageContent, + selection: Binding? + ) { + self.data = data + self.pageViewBuilder = pageViewBuilder + self.selection = selection + } + + public init( + data: [ValueType], + pageViewBuilder: @escaping (ValueType) -> PageContent, + selection: Binding? + ) { + self.data = data + self.pageViewBuilder = { value, _ in pageViewBuilder(value) } + self.selection = selection + } + + + // MARK: Public functions + + func make() -> ViewController { + let viewController = ViewController() + viewController.pageViewBuilder = pageViewBuilder + viewController.modifierData = modifierData + viewController.update(list: data, currentIndex: nil) + setupOnCurrentPageChanged(viewController) + return viewController + } + + func update(viewController: ViewController) { + let selectedIndex = data.enumerated().first { + $0.element.id == selection?.wrappedValue + }?.offset + viewController.modifierData = modifierData + viewController.update(list: data, currentIndex: selectedIndex) + setupOnCurrentPageChanged(viewController) + } + + + // MARK: Private functions + + private func setupOnCurrentPageChanged(_ viewController: ViewController) { + viewController.onCurrentPageChanged = { [data, selection] in + guard $0 >= 0 && $0 < data.count else { return } + selection?.wrappedValue = data[$0].id + } + } +} diff --git a/Lib/SwiftUI/PagingCollectionViewModifierData.swift b/Lib/SwiftUI/PagingCollectionViewModifierData.swift new file mode 100644 index 0000000..0406559 --- /dev/null +++ b/Lib/SwiftUI/PagingCollectionViewModifierData.swift @@ -0,0 +1,45 @@ +// +// PagingCollectionViewModifierData.swift +// CollectionViewPagingLayout +// +// Created by Amir Khorsandi on 10/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import UIKit + +struct PagingCollectionViewModifierData { + var scaleOptions: ScaleTransformViewOptions? + var stackOptions: StackTransformViewOptions? + var snapshotOptions: SnapshotTransformViewOptions? + var snapshotIdentifier: ((Int, UIView?) -> String)? + var canReuseSnapshot: ((SnapshotContainerView, UIView?) -> Bool)? + var numberOfVisibleItems: Int? + var zPositionProvider: ((CGFloat) -> Int)? + var animator: ViewAnimator? + var goToSelectedPage: Bool? + var collectionViewProperties: [CollectionViewPropertyProtocol] = [] + var onTapPage: ((Int) -> Void)? + var scrollDirection: UICollectionView.ScrollDirection? + var pagePadding: PagePadding? + var transparentAttributeWhenCellNotLoaded: Bool? +} + +protocol CollectionViewPropertyProtocol { + func getKey() -> WritableKeyPath? + func getValue() -> T? +} + +struct CollectionViewProperty: CollectionViewPropertyProtocol { + let keyPath: WritableKeyPath + let value: T + + func getKey() -> WritableKeyPath? { + keyPath as? WritableKeyPath + } + + func getValue() -> T? { + value as? T + } +} diff --git a/Lib/SwiftUI/ScalePageView.swift b/Lib/SwiftUI/ScalePageView.swift new file mode 100644 index 0000000..937324b --- /dev/null +++ b/Lib/SwiftUI/ScalePageView.swift @@ -0,0 +1,37 @@ +// +// ScalePageView.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import SwiftUI + +public struct ScalePageView: UIViewControllerRepresentable, TransformPageViewProtocol { + + // MARK: Properties + + public var builder: Builder + + + // MARK: Lifecycle + + public init( + _ data: [ValueType], + selection: Binding? = nil, + @ViewBuilder viewBuilder: @escaping (ValueType) -> PageContent + ) { + builder = .init(data: data, pageViewBuilder: viewBuilder, selection: selection) + builder.modifierData.scaleOptions = .init() + } +} + + +public extension ScalePageView { + func options(_ options: ScaleTransformViewOptions) -> Self { + builder.modifierData.scaleOptions = options + return self + } +} diff --git a/Lib/SwiftUI/SnapshotPageView.swift b/Lib/SwiftUI/SnapshotPageView.swift new file mode 100644 index 0000000..f2c1e87 --- /dev/null +++ b/Lib/SwiftUI/SnapshotPageView.swift @@ -0,0 +1,60 @@ +// +// SnapshotPageView.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import SwiftUI + +public struct SnapshotPageView: UIViewControllerRepresentable, TransformPageViewProtocol { + + // MARK: Properties + + public var builder: Builder + + + // MARK: Lifecycle + + public init( + _ data: [ValueType], + selection: Binding? = nil, + @ViewBuilder viewBuilder: @escaping (ValueType) -> PageContent + ) { + builder = .init(data: data, pageViewBuilder: viewBuilder, selection: selection) + builder.modifierData.snapshotOptions = .init() + } +} + + +public extension SnapshotPageView { + func options(_ options: SnapshotTransformViewOptions) -> Self { + builder.modifierData.snapshotOptions = options + return self + } +} + +public extension SnapshotPageView { + /// A unique identifier for the snapshot, a new snapshot won't be made if + /// there is a cashed snapshot with the same identifier + /// - Parameter index: The index of item + /// - Parameter view: The `UIView` converted from `PageContent` + /// - Returns: Self + func snapshotIdentifier(_ snapshotIdentifier: @escaping (_ index: Int, _ view: UIView?) -> String) -> Self { + builder.modifierData.snapshotIdentifier = snapshotIdentifier + return self + } +} + +public extension SnapshotPageView { + /// Check if the snapshot can be reused + /// - Parameter snapshotContainer: The container for snapshot pieces, see `SnapshotContainerView` + /// - Parameter view: The `UIView` converted from `PageContent` + /// - Returns: Self + func canReuseSnapshot(_ canReuseSnapshot: @escaping (_ snapshotContainer: SnapshotContainerView, _ view: UIView?) -> Bool) -> Self { + builder.modifierData.canReuseSnapshot = canReuseSnapshot + return self + } +} diff --git a/Lib/SwiftUI/StackPageView.swift b/Lib/SwiftUI/StackPageView.swift new file mode 100644 index 0000000..6562acc --- /dev/null +++ b/Lib/SwiftUI/StackPageView.swift @@ -0,0 +1,37 @@ +// +// StackPageView.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import SwiftUI + +public struct StackPageView: UIViewControllerRepresentable, TransformPageViewProtocol { + + // MARK: Properties + + public var builder: Builder + + + // MARK: Lifecycle + + public init( + _ data: [ValueType], + selection: Binding? = nil, + @ViewBuilder viewBuilder: @escaping (ValueType) -> PageContent + ) { + builder = .init(data: data, pageViewBuilder: viewBuilder, selection: selection) + builder.modifierData.stackOptions = .init() + } +} + + +public extension StackPageView { + func options(_ options: StackTransformViewOptions) -> Self { + builder.modifierData.stackOptions = options + return self + } +} diff --git a/Lib/SwiftUI/TransformPageView.swift b/Lib/SwiftUI/TransformPageView.swift new file mode 100644 index 0000000..564e728 --- /dev/null +++ b/Lib/SwiftUI/TransformPageView.swift @@ -0,0 +1,27 @@ +// +// TransformPageView.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// +import Foundation +import SwiftUI + +public struct TransformPageView: UIViewControllerRepresentable, TransformPageViewProtocol { + + // MARK: Properties + + public var builder: Builder + + + // MARK: Lifecycle + + public init( + _ data: [ValueType], + selection: Binding? = nil, + @ViewBuilder viewBuilder: @escaping (ValueType, CGFloat) -> PageContent + ) { + builder = .init(data: data, pageViewBuilder: viewBuilder, selection: selection) + } +} diff --git a/Lib/SwiftUI/TransformPageViewProtocol.swift b/Lib/SwiftUI/TransformPageViewProtocol.swift new file mode 100644 index 0000000..a0dd393 --- /dev/null +++ b/Lib/SwiftUI/TransformPageViewProtocol.swift @@ -0,0 +1,172 @@ +// +// TransformPageViewProtocol.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation +import SwiftUI + +public protocol TransformPageViewProtocol { + associatedtype ValueType: Identifiable + associatedtype PageContent: View + + typealias Builder = PagingCollectionViewControllerBuilder + + var builder: Builder { get } +} + + +public extension TransformPageViewProtocol { + func numberOfVisibleItems(_ count: Int) -> Self { + self.builder.modifierData.numberOfVisibleItems = count + return self + } + + func zPosition(_ zPosition: @escaping (CGFloat) -> Int) -> Self { + self.builder.modifierData.zPositionProvider = zPosition + return self + } + + func onTapPage(_ onTapPage: @escaping (ValueType.ID) -> Void) -> Self { + self.builder.modifierData.onTapPage = { index in + guard index < builder.data.count else { return } + onTapPage(builder.data[index].id) + } + return self + } + + func pagePadding(top: PagePadding.Padding? = nil, + left: PagePadding.Padding? = nil, + bottom: PagePadding.Padding? = nil, + right: PagePadding.Padding? = nil) -> Self { + let current = self.builder.modifierData.pagePadding + let topPadding: PagePadding.Padding? = { + if let top = top { + return top + } else { + return current?.top + } + }() + + let leftPadding: PagePadding.Padding? = { + if let left = left { + return left + } else { + return current?.left + } + }() + + let bottomPadding: PagePadding.Padding? = { + if let bottom = bottom { + return bottom + } else { + return current?.bottom + } + }() + + let rightPadding: PagePadding.Padding? = { + if let right = right { + return right + } else { + return current?.right + } + }() + + self.builder.modifierData.pagePadding = .init( + top: topPadding, + left: leftPadding, + bottom: bottomPadding, + right: rightPadding + ) + return self + } + + func pagePadding(_ padding: PagePadding.Padding? = nil) -> Self { + pagePadding(vertical: padding, horizontal: padding) + } + + func pagePadding(vertical: PagePadding.Padding? = nil, + horizontal: PagePadding.Padding? = nil) -> Self { + let current = self.builder.modifierData.pagePadding + + let topPadding: PagePadding.Padding? = { + if let top = vertical { + return top + } else { + return current?.top + } + }() + + let leftPadding: PagePadding.Padding? = { + if let left = horizontal { + return left + } else { + return current?.left + } + }() + + let bottomPadding: PagePadding.Padding? = { + if let bottom = vertical { + return bottom + } else { + return current?.bottom + } + }() + + let rightPadding: PagePadding.Padding? = { + if let right = horizontal { + return right + } else { + return current?.right + } + }() + + self.builder.modifierData.pagePadding = .init( + top: topPadding, + left: leftPadding, + bottom: bottomPadding, + right: rightPadding + ) + return self + } + + func animator(_ animator: ViewAnimator) -> Self { + self.builder.modifierData.animator = animator + return self + } + + func scrollToSelectedPage(_ goToSelectedPage: Bool) -> Self { + self.builder.modifierData.goToSelectedPage = goToSelectedPage + return self + } + + func scrollDirection(_ direction: UICollectionView.ScrollDirection) -> Self { + self.builder.modifierData.scrollDirection = direction + return self + } + + func hideCellWhenNotLoaded(_ value: Bool) -> Self { + self.builder.modifierData.transparentAttributeWhenCellNotLoaded = value + return self + } + + func collectionView(_ key: WritableKeyPath, _ value: T) -> Self { + let property = CollectionViewProperty(keyPath: key, value: value) + self.builder.modifierData.collectionViewProperties.append(property) + return self + } +} + + +public extension TransformPageViewProtocol where Self: UIViewControllerRepresentable { + func makeUIViewController(context: UIViewControllerRepresentableContext) -> Builder.ViewController { + builder.make() + } + + func updateUIViewController(_ uiViewController: Builder.ViewController, context: Context) { + builder.update(viewController: uiViewController) + } +} diff --git a/Lib/TransformableView.swift b/Lib/TransformableView.swift index 9fd6f8c..f1217d1 100644 --- a/Lib/TransformableView.swift +++ b/Lib/TransformableView.swift @@ -11,15 +11,16 @@ import UIKit public protocol TransformableView { - /// The view for detecting tap gesture - /// when you call `CollectionViewPagingLayout.configureTapOnCollectionView()` - /// a tap gesture will be added to the CollectionView and when the user tap on it - /// it checks if the tap location was in this view frame it will trigger - /// `CollectionViewPagingLayoutDelegate.collectionViewPagingLayout(_ layout:, didSelectItemAt indexPath:)` + /// The view for detecting gestures + /// + /// If you want to handle it manually return `nil` var selectableView: UIView? { get } /// Sends a float value based on the position of the view (cell) /// if the view is in the center of CollectionView it sends 0 + /// the value could be negative or positive and that represents the distance to the center of your CollectionView. + /// for instance `1` means the distance between the center of the cell and the center of your CollectionView + /// is equal to your CollectionView width. /// /// - Parameter progress: the interpolated progress for the cell view func transform(progress: CGFloat) @@ -47,7 +48,20 @@ public extension TransformableView where Self: UICollectionViewCell { /// Default `selectableView` for `UICollectionViewCell` is the first subview of /// `contentView` or the content view itself if there is no subview var selectableView: UIView? { - contentView.subviews.first + contentView.subviews.first ?? contentView } } + +public extension UICollectionViewCell { + /// This method transfers the event to `selectableView` + /// this is necessary since cells are on top of each other and they fill the whole collectionView frame + /// Without this, only the first visible cell is selectable + // swiftlint:disable:next override_in_extension + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let view = (self as? TransformableView)?.selectableView { + return view.hitTest(convert(point, to: view), with: event) + } + return super.hitTest(point, with: event) + } +} diff --git a/Lib/ViewAnimator.swift b/Lib/ViewAnimator.swift new file mode 100644 index 0000000..20a4b2f --- /dev/null +++ b/Lib/ViewAnimator.swift @@ -0,0 +1,133 @@ +// +// ViewAnimator.swift +// CollectionViewPagingLayout +// +// Created by Amir on 04/04/2021 +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import QuartzCore + +/// Simple protocol to define an animator +public protocol ViewAnimator { + /// Animate + /// - Parameter animations: Closure to animate + /// - parameter progress: the animation progress, between 0.0 - 1.0 + /// - parameter finished: animation finished state, set to true at the end + @discardableResult func animate(animations: @escaping (_ progress: Double, _ finished: Bool) -> Void) -> ViewAnimatorCancelable? +} + + +public protocol ViewAnimatorCancelable { + + /// Cancel the animation with changing progress + func cancel() +} + + +/// Default implementation for `ViewAnimator` +public class DefaultViewAnimator: ViewAnimator { + + private let animationDuration: TimeInterval + private let curve: Curve + + private var displayLink: CADisplayLink? + private var start: CFTimeInterval! + private var animationsClosure: ((Double, Bool) -> Void)? + + private var duration: TimeInterval { + #if targetEnvironment(simulator) + return Double(animationDragCoefficient()) * animationDuration + #else + return animationDuration + #endif + } + + public init(_ duration: TimeInterval, curve: Curve) { + self.animationDuration = duration + self.curve = curve + } + + public func animate(animations: @escaping (Double, Bool) -> Void) -> ViewAnimatorCancelable? { + if !Thread.isMainThread { + fatalError("only from main thread") + } + guard duration > 0 else { + animations(1.0, true) + return nil + } + invalidateDisplayLink() + start = CACurrentMediaTime() + animationsClosure = animations + let displayLink = CADisplayLink(target: self, selector: #selector(update)) + displayLink.add(to: .current, forMode: .common) + self.displayLink = displayLink + return Cancelable { [weak self] in + self?.invalidateDisplayLink() + } + } + + @objc private func update() { + guard displayLink != nil else { return } + let delta = CACurrentMediaTime() - start + let progress = curve.fromLinear(progress: delta / duration) + + animationsClosure?(progress, false) + if delta / duration > 1 { + animationsClosure?(progress, true) + invalidateDisplayLink() + } + } + + private func invalidateDisplayLink() { + displayLink?.invalidate() + displayLink = nil + } +} + +public extension DefaultViewAnimator { + enum Curve { + case linear, parametric, easeInOut, easeIn, easeOut + + func fromLinear(progress: Double) -> Double { + let p = min(max(progress, 0), 1) + let result: Double + switch self { + case .linear: + result = p + case .parametric: + result = ((p * p) / (2.0 * ((p * p) - p) + 1.0)) + case .easeInOut: + if p < 0.5 { + result = 2 * p * p + } else { + result = (-2 * p * p) + (4 * p) - 1 + } + case .easeIn: + result = -p * (p - 2) + case .easeOut: + result = p * p + } + return min(max(result, 0), 1) + } + + } +} + +extension DefaultViewAnimator { + private struct Cancelable: ViewAnimatorCancelable { + var onCancel: () -> Void + + func cancel() { + onCancel() + } + } +} + +#if targetEnvironment(simulator) +@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float + +private func animationDragCoefficient() -> Float { + UIAnimationDragCoefficient() +} +#endif diff --git a/Package.swift b/Package.swift index 38c2cf1..ec040a6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "CollectionViewPagingLayout", platforms: [ - .iOS(.v8) + .iOS(.v13) ], products: [ .library( @@ -15,7 +15,6 @@ let package = Package( targets: [ .target( name: "CollectionViewPagingLayout", - path: "Lib", - exclude: ["Samples"]) + path: "Lib") ] ) diff --git a/PrivacyPolicy.md b/PrivacyPolicy.md index 6c4ec37..6afdebc 100644 --- a/PrivacyPolicy.md +++ b/PrivacyPolicy.md @@ -1,5 +1,5 @@ ## Privacy Policy -- This app doesn't collect any data and by removing the app all the data will be permanently removed. -- This app doesn't send any data to any public or private server +- This app doesn't collect any data and by removing the app all local data will be permanently removed. +- This app doesn't send any data to any server diff --git a/README.md b/README.md index cdc9587..81ef52c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CollectionViewPagingLayout +# CollectionViewPagingLayout - PagingView for SwiftUI [![License](https://img.shields.io/cocoapods/l/CollectionViewPagingLayout.svg?style=flat)](http://cocoapods.org/pods/CollectionViewPagingLayout) ![platforms](https://img.shields.io/badge/platforms-iOS-333333.svg) @@ -6,65 +6,86 @@ [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) -## Layout Designer - +## Previews + +### Layout Designer +[](https://apps.apple.com/nl/app/layout-designer/id1507238011?l=en&mt=12) -### SnapshotTransformView +#### Custom implementations, UIKit: `TransformableView`, SwiftUI: `TransformPageView` +Click on image to see the code

- - - - - - - - -

-### ScaleTransformView +[](https://github.com/amirdew/CollectionViewPagingLayout/tree/master/Samples/PagingLayoutSamples/Modules/UIKit/Fruits) +[](https://github.com/amirdew/CollectionViewPagingLayout/tree/master/Samples/PagingLayoutSamples/Modules/UIKit/Gallery) +[](https://github.com/amirdew/CollectionViewPagingLayout/tree/master/Samples/PagingLayoutSamples/Modules/UIKit/Cards) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Samples/PagingLayoutSamples/Modules/SwiftUI/DevicesView.swift) +[](https://github.com/amirdew/CollectionViewPagingLayout/tree/master/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView) +

+ +#### UIKit: `SnapshotTransformView`, SwiftUI: `SnapshotPageView` + +

- - - - - - - - -

-### StackTransformView +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L14) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L15) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L16) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L17) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L18) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L19) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L20) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions%2BLayout.swift#L21) +

+ +#### UIKit: `ScaleTransformView`, SwiftUI: `ScalePageView`

- - - - - - + +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L14) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L15) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L16) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L17) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L18) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L19) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L20) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions%2BLayout.swift#L21)

-## Custom implementations +### UIKit: `StackTransformView`, SwiftUI: `StackPageView`

- - - + +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L14) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L15) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L16) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L17) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L18) +[](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions%2BLayout.swift#L19)

## About -CollectionViewPagingLayout is a simple but powerful tool for making complex layouts for your UICollectionView. -The implementation is very simple, there is a custom `UICollectionViewLayout` that gives you the ability to apply transforms on the cells. -No inheritance or anything like that. -See [How to use](https://github.com/amirdew/CollectionViewPagingLayout#how-to-use) for more details. +**`UIKit:`** +A simple but powerful framework that lets you make complex layouts for your `UICollectionView`. +The implementation is quite simple. Just a custom `UICollectionViewLayout` that gives you the ability to apply transforms to the cells. +No UICollectionView inheritance or anything like that. + +**`SwiftUI:`** +A simple `View` that lets you make page-view effects. +Powered by `UICollectionView` + + + +
+ +For more details, see [How to use](https://github.com/amirdew/CollectionViewPagingLayout#how-to-use) ## Installation -CollectionViewPagingLayout doesn't contain any external dependencies. +This framework doesn't contain any external dependencies. #### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html) @@ -106,171 +127,33 @@ Just add all the files under `Lib` directory to your project ## How to use -### Using Layout Designer +### Using [Layout Designer](https://apps.apple.com/nl/app/layout-designer/id1507238011?l=en&mt=1) -There is a macOS app to make it even easier for you to build your own custom layout. -It allows you to tweak many options and see the result in real time. -- You can [download](https://apps.apple.com/nl/app/layout-designer/id1507238011?l=en&mt=1) the app from App Store and support this repository or build it yourself from the source. -- Open the app, tweak options and design your layout. -- Copy the code from the right panel and paste it on Xcode or click on "save it as project" and open it with Xcode. -- Don't forget to [install](https://github.com/amirdew/CollectionViewPagingLayout#installation) the libray. - - -### Manually - -- Make sure you imported the library -```swift -import CollectionViewPagingLayout -``` -- Set up your `UICollectionView` as you always do (you need to use a custom class for cells) -- Set the layout for your collection view: -(in most cases you want a paging effect so enable that too) -```swift -let layout = CollectionViewPagingLayout() -collectionView.collectionViewLayout = layout -collectionView.isPagingEnabled = true // enabling paging effect -``` - -- Now you just need to conform your `UICollectionViewCell` class to `TransformableView` and start implementing your custom transforms. By conforming your cell class to `TransformableView` protocol you will get a `progress` value and you can use it to apply any changes on your cell view. - - -*Note:* you can use [Prepared Transformable Protocols](#prepared-transformable-protocols) instead of `TransformableView` if you want to use prepared effects! +There is a macOS app to make it even easier for you to build your custom layout. +It allows you to tweak many options and see the result in real-time. +It also generates the code for you. So, you can copy it to your project. -```swift -extension MyCollectionViewCell: TransformableView { - func transform(progress: CGFloat) { - ... - } -} -``` -> `progress` is a float value that represents the current position of your cell in the collection view. -> When it's `0` that means the current position of the cell is exactly in the center of your collection view. -> the value could be negative or positive and that represents the distance to the center of your collection view. -> for instance `1` means the distance between the center of the cell and the center of your collection view is equal to your collection view width. - - -you can start with a simple transform like this: -```swift -extension MyCollectionViewCell: TransformableView { - func transform(progress: CGFloat) { - let transform = CGAffineTransform(translationX: bounds.width/2 * progress, y: 0) - let alpha = 1 - abs(progress) - - contentView.subviews.forEach { $0.transform = transform } - contentView.alpha = alpha - } -} -``` - -- Don't forget to set `numberOfVisibleItems`, by default it's null and that means it will load all of the cells at a time -```swift -layout.numberOfVisibleItems = ... -``` - -## Prepared Transformable Protocols - -There are prepared transformables to make it easier to use this library, -using them is very simple, you just need to conform your `UICollectionViewCell` to the prepared protocol -and then set the options for that to customize it as you want. -there are three types of transformables protocol at the moment `ScaleTransformView`, `SnapshotTransformView`, and `StackTransformView`. -as you can see in the samples app these protocols are highly customizable and you can make tons of different effects with them. -here is a simple example for `ScaleTransformView` which gives you a simple paging with scaling effect: -```swift -extension MyCollectionViewCell: ScaleTransformView { - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.6, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.66, y: 0.2), - maxTranslationRatio: CGPoint(x: 2, y: 0), - ) -} -``` -there is an options struct for each transformable where you can customize the effect, check the struct to find out what each parameter does. -a short comment on the top of each parameter explains how you can use it. -`ScaleTransformView` -> [`ScaleTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Scale/ScaleTransformViewOptions.swift) -`SnapshotTransformView` -> [`SnapshotTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Snapshot/SnapshotTransformViewOptions.swift) -`StackTransformView` -> [`StackTransformViewOptions`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Lib/Stack/StackTransformViewOptions.swift) - -you can see some examples in the samples app for these transformables. -check [here](https://github.com/amirdew/CollectionViewPagingLayout/tree/master/PagingLayoutSamples/Modules/Shapes/ShapeCell) to see used options for each: `/PagingLayoutSamples/Modules/Shapes/ShapeCell/` - -#### Target view -You may wonder how does it figure out the view for applying transforms on, if you check each transformable protocol you can see the target views are defined for each, you can also see there is an extension to provide the default target views. -for instance we have `ScaleTransformView.scalableView` which is the view that we apply scale transforms on, and for `UICollectionViewCell` the default view is the first subview of `contentView`: - -```swift -public extension ScaleTransformView where Self: UICollectionViewCell { - - /// Default `scalableView` for `UICollectionViewCell` is the first subview of - /// `contentView` or the content view itself if there is no subview - var scalableView: UIView { - contentView.subviews.first ?? contentView - } -} -``` -of course you can easily override this - - -## Customize Prepared Transformables - -Yes, you can customize them or even combine them, to do that like before conform your cell class to the transformable protocol(s) and then implement `TransformableView.transform` function and call the transformable function manually, like this: -```swift -extension LayoutTypeCollectionViewCell: ScaleTransformView { - - func transform(progress: CGFloat) { - applyScaleTransform(progress: progress) - // customize views here, like this: - titleLabel.alpha = 1 - abs(progress) - subtitleLabel.alpha = titleLabel.alpha - } - -} -``` -as you can see `applyScaleTransform` applies the scale transforms and right after that we change the alpha for `titleLabel` and `subtitleLabel`. -to find the public function(s) for each tansformable check the protocol definition. - -## Other features - -### Control current page - -You can control the current page by the following functions of `CollectionViewPagingLayout`: -- `func setCurrentPage(_ page: Int, animated: Bool = true)` -- `func goToNextPage(animated: Bool = true)` -- `func goToPreviousPage(animated: Bool = true)` - -these are safe wrappers for setting `ContentOffset` of `UICollectionview` -you can also get current page by a public variable `CollectionViewPagingLayout.currentPage` or listen to changes by using `CollectionViewPagingLayout.delegate`: -```swift -public protocol CollectionViewPagingLayoutDelegate: class { - func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) -} -``` +You can [purchase](https://apps.apple.com/nl/app/layout-designer/id1507238011?l=en&mt=1) the app from App Store and support this repository, +or you can build it yourself from the source. +Yes, the macOS app is open-source too!. -### Handle touches -There is built-in solution for handling tap gestues on the views -- Implement `TransformableView.selectableView` and pass the view that you would like to handle tap gestues for (by default this is the first subview of your `UICollectionViewCell.contentView`) -- Call `CollectionViewPagingLayout.configureTapOnCollectionView()` after setting the layout for you collection view -- Set the delegate for layout (`CollectionViewPagingLayout.delegate`) -- Use didSelectItemAt function `func collectionViewPagingLayout(_ layout: CollectionViewPagingLayout, didSelectItemAt indexPath: IndexPath)` +Continue for [`SwiftUI`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_SWIFTUI.md) or [`UIKit`](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/HOW_TO_USE_UIKIT.md) ## Limitations -- Specify the number of visible cells: -You need to specify the number of visible cells since this layout gives you the flexibility to show the next and previous cells. -By default, the layout loads all of the cells in the collection view frame and that means it keeps all of them in memory. -You can specify the number of cells that you need to show at a time by considering your design. +- **Specify the number of visible items:** +You need to specify the number of visible items. +Since this layout gives you the flexibility to show the next and previous cells, +By default, it loads all of the cells in the collectionview's frame, which means iOS keeps all of them in the memory. +Based on your design, you can specify the number of items that you need to show. -- Touches on cells: -The way that this layout works is putting all the cells in the collectionview frame (doesn't matter which TransformView you use) -and then it applies transforms on the target view (StackTransformView.cardView, ScaleTransformView.scalableView etc). -so you can use `func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)` but only for one cell at the same time -you can implement `func zPosition(progress: CGFloat) -> Int` to specify wich cell should be on top -this also means you can't handle any gesture for the cell that is not on the top! +- **It doesn't support RTL layouts:** +however, you can achieve a similar result by tweaking options, for instance try `StackTransformViewOptions.Layout.reverse` -if you would like to handle tap on multiple cells at the same time see [Handle touches](https://github.com/amirdew/CollectionViewPagingLayout#handle-touches) +## Credit -- It doesn't support RTL layouts +- [DevicesView](https://github.com/amirdew/CollectionViewPagingLayout/blob/master/Samples/PagingLayoutSamples/Modules/SwiftUI/DevicesView.swift) inspired by this [Cuberto's post](https://dribbble.com/shots/12580831-Principle-Tutorial-Onboarding-Flow-Animation) ## License diff --git a/Samples/AppKitGlue/Info.plist b/Samples/AppKitGlue/Info.plist index 977fbbd..ca5c428 100644 --- a/Samples/AppKitGlue/Info.plist +++ b/Samples/AppKitGlue/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright Copyright © 2020 Amir Khorsandi. All rights reserved. NSPrincipalClass diff --git a/Samples/AppKitGlue/MacApp.swift b/Samples/AppKitGlue/MacApp.swift index 557690d..647c6d8 100644 --- a/Samples/AppKitGlue/MacApp.swift +++ b/Samples/AppKitGlue/MacApp.swift @@ -7,34 +7,53 @@ // import Cocoa +import Combine class MacApp: NSObject, AppKitBridge { - + + private var window: NSWindow! + private var cancellable: Cancellable! + func initialise() { - DispatchQueue.main.async { + cancellable = DispatchQueue.main.schedule(after: .init(.now()), interval: .milliseconds(10)) { if NSApplication.shared.mainWindow != nil { + self.window = NSApplication.shared.mainWindow self.onMainWindowReady() } } } private func onMainWindowReady() { + cancellable.cancel() hideToolbar() setSize() + addVisualEffectView() } private func setSize() { - guard let window = NSApplication.shared.mainWindow else { - return + if window.minSize.width < 1_200 || window.minSize.height < 768 { + window.minSize = NSSize(width: 1_200, height: 768) + } + if window.frame.size.width < window.minSize.width || + window.frame.size.height < window.minSize.height { + let size = NSSize(width: max(window.minSize.width, window.frame.size.width), + height: max(window.minSize.height, window.frame.size.height)) + window.setFrame(.init(origin: window.frame.origin, size: size), display: true, animate: true) } - window.minSize = NSSize(width: 1_200, height: 768) - window.setFrame(.init(origin: window.frame.origin, size: window.minSize), display: true, animate: true) } private func hideToolbar() { - if let window = NSApplication.shared.mainWindow { - window.titleVisibility = .hidden - window.toolbar = nil - } + window.titleVisibility = .hidden + window.toolbar = nil + } + + private func addVisualEffectView() { + let visualEffectView = NSVisualEffectView(frame: .zero) + visualEffectView.blendingMode = .behindWindow + visualEffectView.material = .hudWindow + visualEffectView.appearance = NSAppearance(named: .vibrantDark) + window.contentView?.addSubview(visualEffectView, positioned: .below, relativeTo: nil) + visualEffectView.frame = window.contentView?.bounds ?? .zero + visualEffectView.autoresizingMask = [.width, .height] } } diff --git a/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj b/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj index 89a9542..829be8e 100644 --- a/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj +++ b/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj @@ -15,9 +15,13 @@ 2925CDDF23D4D21F00243F5F /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2925CDDE23D4D21F00243F5F /* Card.swift */; }; 2949CB272476EA8C000CC073 /* UITableView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2949CB262476EA8C000CC073 /* UITableView+Utilities.swift */; }; 294BA3C324A77A73008D0569 /* LayoutDesignerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294BA3C224A77A73008D0569 /* LayoutDesignerViewModel.swift */; }; + 29519ACC262B5DE400D8A4A3 /* ShapesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29519ACB262B5DE400D8A4A3 /* ShapesListView.swift */; }; + 29519ACE262B646E00D8A4A3 /* ShapesListView.ShapeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29519ACD262B646E00D8A4A3 /* ShapesListView.ShapeView.swift */; }; + 296EF99A2628B13000B72439 /* WeatherTabView.PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296EF9992628B13000B72439 /* WeatherTabView.PageView.swift */; }; 2977657C2474531200835DBD /* LayoutDesignerOptionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2977657B2474531200835DBD /* LayoutDesignerOptionCell.swift */; }; 2977657E2474531D00835DBD /* LayoutDesignerOptionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2977657D2474531D00835DBD /* LayoutDesignerOptionCell.xib */; }; 29776580247454BC00835DBD /* LayoutDesignerOptionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2977657F247454BC00835DBD /* LayoutDesignerOptionCellViewModel.swift */; }; + 29834F2B25C5977300896343 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29834F2A25C5977300896343 /* DevicesView.swift */; }; 2993722324A79A9C0026D52F /* ShapeLayout+ScaleOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2993722224A79A9C0026D52F /* ShapeLayout+ScaleOptions.swift */; }; 29A2D3AA24B72895005A0F6B /* LayoutDesignerCodePreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29A2D3A924B72895005A0F6B /* LayoutDesignerCodePreviewViewController.xib */; }; 29A2D3CD24B738CC005A0F6B /* LayoutDesignerIntroViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A2D3CC24B738CC005A0F6B /* LayoutDesignerIntroViewController.swift */; }; @@ -44,6 +48,11 @@ 29D9F95323F8685C00656A67 /* BaseShapeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95223F8685C00656A67 /* BaseShapeCollectionViewCell.swift */; }; 29D9F95923F874E900656A67 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95823F874E900656A67 /* GradientView.swift */; }; 29D9F95B23F88A6900656A67 /* ShapeCollectionViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95A23F88A6900656A67 /* ShapeCollectionViewCells.swift */; }; + 29DD1E18262597EF00846F7B /* WeatherTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD1E17262597EF00846F7B /* WeatherTabView.swift */; }; + 29DD1E2B26275E1D00846F7B /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD1E2A26275E1D00846F7B /* VisualEffectView.swift */; }; + 29DD1E2F2627702C00846F7B /* WeatherTabView.Overlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD1E2E2627702C00846F7B /* WeatherTabView.Overlay.swift */; }; + 29DD1E342627708B00846F7B /* WeatherPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD1E332627708B00846F7B /* WeatherPage.swift */; }; + 29DD1E3A2627774400846F7B /* WeatherTabView.TabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD1E392627774400846F7B /* WeatherTabView.TabView.swift */; }; 29FF296224A6321100C83DF9 /* LayoutDesignerCodePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF296124A6321100C83DF9 /* LayoutDesignerCodePreviewViewController.swift */; }; 29FF296424A6321B00C83DF9 /* LayoutDesignerCodePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF296324A6321B00C83DF9 /* LayoutDesignerCodePreviewViewModel.swift */; }; 55923226155A0B6E5A55C691 /* Pods_PagingLayoutSamples.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A489BA28D16931A0BE34BD0B /* Pods_PagingLayoutSamples.framework */; }; @@ -125,9 +134,13 @@ 2925CDDE23D4D21F00243F5F /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; 2949CB262476EA8C000CC073 /* UITableView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Utilities.swift"; sourceTree = ""; }; 294BA3C224A77A73008D0569 /* LayoutDesignerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerViewModel.swift; sourceTree = ""; }; + 29519ACB262B5DE400D8A4A3 /* ShapesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesListView.swift; sourceTree = ""; }; + 29519ACD262B646E00D8A4A3 /* ShapesListView.ShapeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesListView.ShapeView.swift; sourceTree = ""; }; + 296EF9992628B13000B72439 /* WeatherTabView.PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTabView.PageView.swift; sourceTree = ""; }; 2977657B2474531200835DBD /* LayoutDesignerOptionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerOptionCell.swift; sourceTree = ""; }; 2977657D2474531D00835DBD /* LayoutDesignerOptionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayoutDesignerOptionCell.xib; sourceTree = ""; }; 2977657F247454BC00835DBD /* LayoutDesignerOptionCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerOptionCellViewModel.swift; sourceTree = ""; }; + 29834F2A25C5977300896343 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 2993722224A79A9C0026D52F /* ShapeLayout+ScaleOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShapeLayout+ScaleOptions.swift"; sourceTree = ""; }; 29A2D3A924B72895005A0F6B /* LayoutDesignerCodePreviewViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayoutDesignerCodePreviewViewController.xib; sourceTree = ""; }; 29A2D3CC24B738CC005A0F6B /* LayoutDesignerIntroViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerIntroViewController.swift; sourceTree = ""; }; @@ -154,6 +167,11 @@ 29D9F95223F8685C00656A67 /* BaseShapeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseShapeCollectionViewCell.swift; sourceTree = ""; }; 29D9F95823F874E900656A67 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; 29D9F95A23F88A6900656A67 /* ShapeCollectionViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeCollectionViewCells.swift; sourceTree = ""; }; + 29DD1E17262597EF00846F7B /* WeatherTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTabView.swift; sourceTree = ""; }; + 29DD1E2A26275E1D00846F7B /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; + 29DD1E2E2627702C00846F7B /* WeatherTabView.Overlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTabView.Overlay.swift; sourceTree = ""; }; + 29DD1E332627708B00846F7B /* WeatherPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherPage.swift; sourceTree = ""; }; + 29DD1E392627774400846F7B /* WeatherTabView.TabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTabView.TabView.swift; sourceTree = ""; }; 29FF296124A6321100C83DF9 /* LayoutDesignerCodePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerCodePreviewViewController.swift; sourceTree = ""; }; 29FF296324A6321B00C83DF9 /* LayoutDesignerCodePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerCodePreviewViewModel.swift; sourceTree = ""; }; 444EC9E8B3BA262F3697984F /* Pods-PagingLayoutSamples.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PagingLayoutSamples.release.xcconfig"; path = "Target Support Files/Pods-PagingLayoutSamples/Pods-PagingLayoutSamples.release.xcconfig"; sourceTree = ""; }; @@ -252,6 +270,15 @@ path = AppKitGlue; sourceTree = ""; }; + 29519ACA262B5DD400D8A4A3 /* Shapes */ = { + isa = PBXGroup; + children = ( + 29519ACB262B5DE400D8A4A3 /* ShapesListView.swift */, + 29519ACD262B646E00D8A4A3 /* ShapesListView.ShapeView.swift */, + ); + path = Shapes; + sourceTree = ""; + }; 2977657A247452BC00835DBD /* Options */ = { isa = PBXGroup; children = ( @@ -261,6 +288,16 @@ path = Options; sourceTree = ""; }; + 29834F2925C5976300896343 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 29519ACA262B5DD400D8A4A3 /* Shapes */, + 29DD1E322627707A00846F7B /* WeatherTabView */, + 29834F2A25C5977300896343 /* DevicesView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 29A2D3CB24B73870005A0F6B /* Intro */ = { isa = PBXGroup; children = ( @@ -317,6 +354,29 @@ path = ShapeCell; sourceTree = ""; }; + 29DD1E21262753B900846F7B /* UIKit */ = { + isa = PBXGroup; + children = ( + 29D9F94123F7F92000656A67 /* Shapes */, + AB500A4823B13B9D0056BE37 /* Fruits */, + AB1BBA8B23C935BF004E5C3B /* Cards */, + ABC242C123B681F800DBD4D6 /* Gallery */, + ); + path = UIKit; + sourceTree = ""; + }; + 29DD1E322627707A00846F7B /* WeatherTabView */ = { + isa = PBXGroup; + children = ( + 29DD1E17262597EF00846F7B /* WeatherTabView.swift */, + 29DD1E392627774400846F7B /* WeatherTabView.TabView.swift */, + 29DD1E2E2627702C00846F7B /* WeatherTabView.Overlay.swift */, + 296EF9992628B13000B72439 /* WeatherTabView.PageView.swift */, + 29DD1E332627708B00846F7B /* WeatherPage.swift */, + ); + path = WeatherTabView; + sourceTree = ""; + }; 29FF296024A631DC00C83DF9 /* Code */ = { isa = PBXGroup; children = ( @@ -435,12 +495,10 @@ AB500A5223B152170056BE37 /* Modules */ = { isa = PBXGroup; children = ( + 29DD1E21262753B900846F7B /* UIKit */, + 29834F2925C5976300896343 /* SwiftUI */, 292489AE2461A96600A316B0 /* LayoutDesigner */, AB7C1E0423B4E292006441DE /* Main */, - 29D9F94123F7F92000656A67 /* Shapes */, - AB500A4823B13B9D0056BE37 /* Fruits */, - AB1BBA8B23C935BF004E5C3B /* Cards */, - ABC242C123B681F800DBD4D6 /* Gallery */, ); path = Modules; sourceTree = ""; @@ -457,6 +515,7 @@ 29B5A72724A8D8F700C9843E /* TransformCurve+Name.swift */, 29B5A72B24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift */, 29B5A72924A8D94300C9843E /* Values+Pair.swift */, + 29DD1E2A26275E1D00846F7B /* VisualEffectView.swift */, ); path = Utilities; sourceTree = ""; @@ -578,7 +637,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1120; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = "Amir Khorsandi"; TargetAttributes = { 292489B62461F07D00A316B0 = { @@ -723,6 +782,7 @@ 2925CDDF23D4D21F00243F5F /* Card.swift in Sources */, 29D9F95023F806C400656A67 /* Optional+Let.swift in Sources */, ABA1A72C23B42240006A46A3 /* PriceTagView.swift in Sources */, + 29DD1E2F2627702C00846F7B /* WeatherTabView.Overlay.swift in Sources */, ABA0DA0C23F98C78004A9C18 /* ShapeCardViewModel.swift in Sources */, 29A2D3CD24B738CC005A0F6B /* LayoutDesignerIntroViewController.swift in Sources */, ABA0DA0E23F98ECD004A9C18 /* UICollectionViewCell+Utilities.swift in Sources */, @@ -733,6 +793,7 @@ AB1BBA9B23CA5179004E5C3B /* CardCellViewModel.swift in Sources */, AB500A5023B151780056BE37 /* FruitsViewModel.swift in Sources */, 2977657C2474531200835DBD /* LayoutDesignerOptionCell.swift in Sources */, + 29519ACE262B646E00D8A4A3 /* ShapesListView.ShapeView.swift in Sources */, 2949CB272476EA8C000CC073 /* UITableView+Utilities.swift in Sources */, 29B5A72A24A8D94300C9843E /* Values+Pair.swift in Sources */, AB500A4A23B13BBD0056BE37 /* FruitsViewController.swift in Sources */, @@ -742,6 +803,7 @@ 29B5A72824A8D8F700C9843E /* TransformCurve+Name.swift in Sources */, AB500A4E23B13C5D0056BE37 /* NibBased.swift in Sources */, ABC242C723B6823600DBD4D6 /* GalleryViewModel.swift in Sources */, + 296EF99A2628B13000B72439 /* WeatherTabView.PageView.swift in Sources */, AB1BBA9E23CA7BD9004E5C3B /* CardsViewController.swift in Sources */, ABA0DA0323F93CDB004A9C18 /* Shape.swift in Sources */, ABC242CA23B682DD00DBD4D6 /* PhotoCellViewModel.swift in Sources */, @@ -757,19 +819,25 @@ ABA0DA0823F98B65004A9C18 /* ShapeCardView.swift in Sources */, ABC242CC23B6831400DBD4D6 /* Photo.swift in Sources */, 29B5A72424A8CC4B00C9843E /* CGFloat+String.swift in Sources */, + 29834F2B25C5977300896343 /* DevicesView.swift in Sources */, + 29519ACC262B5DE400D8A4A3 /* ShapesListView.swift in Sources */, 29FF296224A6321100C83DF9 /* LayoutDesignerCodePreviewViewController.swift in Sources */, 29D9F94323F7F98800656A67 /* ShapesViewController.swift in Sources */, AB500A5823B154210056BE37 /* Fruit.swift in Sources */, 29B5A72C24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift in Sources */, 29B5A72624A8D8B300C9843E /* OptionsCodeGenerator.swift in Sources */, 29D9F94523F7F99400656A67 /* ShapesViewModel.swift in Sources */, + 29DD1E18262597EF00846F7B /* WeatherTabView.swift in Sources */, 29B5A72024A7B02900C9843E /* ShapeLayout+StackOptions.swift in Sources */, 29A2D3D124B738E5005A0F6B /* LayoutDesignerIntroViewModel.swift in Sources */, AB500A5B23B154640056BE37 /* FruitsCollectionViewCell.swift in Sources */, ABA1A73123B422B2006A46A3 /* QuantityControllerView.swift in Sources */, + 29DD1E342627708B00846F7B /* WeatherPage.swift in Sources */, 29BEC4D52476DD9D004BA505 /* LayoutDesignerOptionsTableView.swift in Sources */, + 29DD1E2B26275E1D00846F7B /* VisualEffectView.swift in Sources */, 292489B02461A97900A316B0 /* LayoutDesignerViewController.swift in Sources */, 29D9F95923F874E900656A67 /* GradientView.swift in Sources */, + 29DD1E3A2627774400846F7B /* WeatherTabView.TabView.swift in Sources */, 29FF296424A6321B00C83DF9 /* LayoutDesignerCodePreviewViewModel.swift in Sources */, AB500A5523B152500056BE37 /* ViewModelBased.swift in Sources */, 292489C52461FE2700A316B0 /* Catalyst.swift in Sources */, @@ -878,6 +946,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -885,6 +954,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 150; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -902,7 +972,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.5.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -939,6 +1010,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -946,6 +1018,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 150; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -957,7 +1030,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.5.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -973,22 +1047,23 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "PagingLayoutSamples/Paging Layout.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 4J5W7CJ2ZV; INFOPLIST_FILE = PagingLayoutSamples/Info.plist; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = app.amir.paginglayout; PRODUCT_NAME = "Layout Designer"; SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "PagingLayoutSamples/PagingLayoutSamples-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Debug; }; @@ -998,22 +1073,23 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "PagingLayoutSamples/Paging Layout.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 4J5W7CJ2ZV; INFOPLIST_FILE = PagingLayoutSamples/Info.plist; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = app.amir.paginglayout; PRODUCT_NAME = "Layout Designer"; SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "PagingLayoutSamples/PagingLayoutSamples-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Release; }; diff --git a/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme b/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme index 2cc40ad..c405f46 100644 --- a/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme +++ b/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme @@ -1,6 +1,6 @@ Bool { window = UIWindow() + window?.backgroundColor = .clear navigationController = UINavigationController() + navigationController.view.backgroundColor = .clear navigationController.isNavigationBarHidden = true - let mainVC = UIDevice.current.userInterfaceIdiom == .pad ? + let mainVC = UIDevice.current.userInterfaceIdiom != .phone ? LayoutDesignerViewController.instantiate(viewModel: LayoutDesignerViewModel()) : MainViewController.instantiate() - + + #if targetEnvironment(macCatalyst) + if let titlebar = window?.windowScene?.titlebar { + titlebar.titleVisibility = .hidden + titlebar.toolbar = nil + } + #endif navigationController.setViewControllers([mainVC], animated: false) window!.rootViewController = navigationController + // UIHostingController(rootView: DevicesView().ignoresSafeArea()) window!.makeKeyAndVisible() return true } diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..f67f483 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "186", + "red" : "19" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6.png new file mode 100644 index 0000000..767efad Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_128.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_128.png new file mode 100644 index 0000000..2b773d0 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_128.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_16.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_16.png new file mode 100644 index 0000000..85a9540 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_16.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256-1.png new file mode 100644 index 0000000..7e75ced Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256.png new file mode 100644 index 0000000..7e75ced Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_256.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32-1.png new file mode 100644 index 0000000..67b4cbc Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32.png new file mode 100644 index 0000000..67b4cbc Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_32.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512-1.png new file mode 100644 index 0000000..dd823d8 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512.png new file mode 100644 index 0000000..dd823d8 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_512.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_64.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_64.png new file mode 100644 index 0000000..4393e74 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard Copy 6_64.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard.png new file mode 100644 index 0000000..3ab72cd Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@1x.png new file mode 100644 index 0000000..d90ba24 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@1x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x-1.png new file mode 100644 index 0000000..a5ead61 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x.png new file mode 100644 index 0000000..a5ead61 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@3x.png new file mode 100644 index 0000000..1e6f89a Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_20pt@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@1x.png new file mode 100644 index 0000000..4cc50ff Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@1x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x-1.png new file mode 100644 index 0000000..ecb8579 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x.png new file mode 100644 index 0000000..ecb8579 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@3x.png new file mode 100644 index 0000000..dec2de0 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_29pt@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@1x.png new file mode 100644 index 0000000..a5ead61 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@1x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x-1.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x-1.png new file mode 100644 index 0000000..0609b31 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x-1.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x.png new file mode 100644 index 0000000..0609b31 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@3x.png new file mode 100644 index 0000000..373e707 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_40pt@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@2x.png new file mode 100644 index 0000000..373e707 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@3x.png new file mode 100644 index 0000000..1a7a5d3 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_60pt@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@1x.png new file mode 100644 index 0000000..58f3331 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@1x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@2x.png new file mode 100644 index 0000000..cdba86d Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_76pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_83pt@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_83pt@2x.png new file mode 100644 index 0000000..3a2118a Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Artboard_83pt@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Contents.json index de20adc..f824938 100644 --- a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1 +1,176 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"icon_20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"icon_20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"icon_29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"icon_29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"icon_40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"icon_40x40@3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"icon_60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"icon_60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"icon_20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"icon_20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"icon_29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"icon_29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"icon_40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"icon_40x40@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"icon_76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"icon_76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"icon_83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"icon_1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file +{ + "images" : [ + { + "filename" : "Artboard_20pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Artboard_20pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Artboard_29pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Artboard_29pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Artboard_40pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Artboard_40pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Artboard_60pt@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Artboard_60pt@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Artboard_20pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Artboard_20pt@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Artboard_29pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Artboard_29pt@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Artboard_40pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Artboard_40pt@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Artboard_76pt@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Artboard_76pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Artboard_83pt@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "Artboard.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "Artboard Copy 6_16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Artboard Copy 6_32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Artboard Copy 6_32-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Artboard Copy 6_64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Artboard Copy 6_128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Artboard Copy 6_256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Artboard Copy 6_256-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Artboard Copy 6_512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Artboard Copy 6_512-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Artboard Copy 6.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_1024x1024@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_1024x1024@1x.png deleted file mode 100644 index e76fcf7..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_1024x1024@1x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@1x.png deleted file mode 100644 index fc30987..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@1x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png deleted file mode 100644 index 5a86302..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png deleted file mode 100644 index 29ddbc0..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_20x20@3x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@1x.png deleted file mode 100644 index dd5caa0..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@1x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png deleted file mode 100644 index 974bf0f..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png deleted file mode 100644 index ec0bdf3..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_29x29@3x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@1x.png deleted file mode 100644 index 5a86302..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@1x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png deleted file mode 100644 index e2f268c..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png deleted file mode 100644 index 01cdce8..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png deleted file mode 100644 index 01cdce8..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png deleted file mode 100644 index c290121..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@1x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@1x.png deleted file mode 100644 index 3b81d78..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@1x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png deleted file mode 100644 index 2611f74..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png deleted file mode 100644 index c48a4a9..0000000 Binary files a/Samples/PagingLayoutSamples/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png and /dev/null differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Colors/Background.colorset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Colors/Background.colorset/Contents.json index 377daa5..79225bf 100644 --- a/Samples/PagingLayoutSamples/Assets.xcassets/Colors/Background.colorset/Contents.json +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Colors/Background.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { - "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { - "red" : "0xF3", "alpha" : "1.000", - "blue" : "0xF3", - "green" : "0xF3" + "blue" : "243", + "green" : "243", + "red" : "243" } - } + }, + "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/Samples/PagingLayoutSamples/Assets.xcassets/Contents.json +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/Contents.json new file mode 100644 index 0000000..2ab326e --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "WikipediaLogo.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/WikipediaLogo.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/WikipediaLogo.png new file mode 100644 index 0000000..e18827b Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/WikipediaLogo.imageset/WikipediaLogo.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Image.png new file mode 100644 index 0000000..0870e67 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning1.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Image.png new file mode 100644 index 0000000..97c7fb9 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/lightning2.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Image.png new file mode 100644 index 0000000..54eec62 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon1.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Image.png new file mode 100644 index 0000000..b6537bd Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/moon2.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Image.png new file mode 100644 index 0000000..69c60d7 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow1.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Image.png new file mode 100644 index 0000000..edb1648 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/snow2.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Image.png new file mode 100644 index 0000000..f1be408 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun1.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Image.png new file mode 100644 index 0000000..a1b9f85 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/sun2.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Image.png new file mode 100644 index 0000000..7390fc4 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado1.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Contents.json new file mode 100644 index 0000000..62beec5 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Image.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Image.png b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Image.png new file mode 100644 index 0000000..71fd5c5 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Weather/tornado2.imageset/Image.png differ diff --git a/Samples/PagingLayoutSamples/Base.lproj/LaunchScreen.storyboard b/Samples/PagingLayoutSamples/Base.lproj/LaunchScreen.storyboard index 346fff9..a2ebcf3 100644 --- a/Samples/PagingLayoutSamples/Base.lproj/LaunchScreen.storyboard +++ b/Samples/PagingLayoutSamples/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + @@ -16,15 +16,15 @@ - + + - + - diff --git a/Samples/PagingLayoutSamples/CustomViews/QuantityControllerView/QuantityControllerView.swift b/Samples/PagingLayoutSamples/CustomViews/QuantityControllerView/QuantityControllerView.swift index faa715e..f36dc9e 100644 --- a/Samples/PagingLayoutSamples/CustomViews/QuantityControllerView/QuantityControllerView.swift +++ b/Samples/PagingLayoutSamples/CustomViews/QuantityControllerView/QuantityControllerView.swift @@ -8,7 +8,7 @@ import UIKit -protocol QuantityControllerViewDelegate: class { +protocol QuantityControllerViewDelegate: AnyObject { func onIncreaseButtonTouched(view: QuantityControllerView) func onDecreaseButtonTouched(view: QuantityControllerView) } diff --git a/Samples/PagingLayoutSamples/Info.plist b/Samples/PagingLayoutSamples/Info.plist index 8b9cf88..11347b9 100644 --- a/Samples/PagingLayoutSamples/Info.plist +++ b/Samples/PagingLayoutSamples/Info.plist @@ -18,6 +18,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + LSApplicationCategoryType + public.app-category.developer-tools LSRequiresIPhoneOS UILaunchStoryboardName @@ -40,8 +42,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - LSApplicationCategoryType - public.app-category.developer-tools UIUserInterfaceStyle Light diff --git a/Samples/PagingLayoutSamples/Models/Shape.swift b/Samples/PagingLayoutSamples/Models/Shape.swift index 4a23797..46c24cd 100644 --- a/Samples/PagingLayoutSamples/Models/Shape.swift +++ b/Samples/PagingLayoutSamples/Models/Shape.swift @@ -6,7 +6,11 @@ // Copyright © 2020 Amir Khorsandi. All rights reserved. // -struct Shape { +struct Shape: Identifiable { let name: String let iconName: String + + var id: String { + name + } } diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift index 899a9ee..4ff69f0 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift @@ -16,7 +16,7 @@ protocol LayoutDesignerCodePreviewViewControllerDelegate: AnyObject { class LayoutDesignerCodePreviewViewController: UIViewController, NibBased, ViewModelBased { - + // MARK: Properties var viewModel: LayoutDesignerCodePreviewViewModel! { @@ -50,8 +50,8 @@ class LayoutDesignerCodePreviewViewController: UIViewController, NibBased, ViewM @IBAction private func saveButtonTouched() { guard let exportURL = viewModel.sampleProjectTempURL else { return } - viewModel.generateSampleProject() - let controller = UIDocumentPickerViewController(url: exportURL, in: UIDocumentPickerMode.exportToService) + viewModel.generateSampleProject(type: selectedCodeType()) + let controller = UIDocumentPickerViewController(forExporting: [exportURL]) controller.delegate = self present(controller, animated: true) } @@ -82,22 +82,40 @@ class LayoutDesignerCodePreviewViewController: UIViewController, NibBased, ViewM private func configureCodeTypeSegmentedControl() { codeModeSegmentedControl.backgroundColor = UIColor.black.withAlphaComponent(0.4) codeModeSegmentedControl.selectedSegmentTintColor = UIColor.white.withAlphaComponent(0.4) - codeModeSegmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .normal) - codeModeSegmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.black.withAlphaComponent(0.6)], for: .selected) - codeModeSegmentedControl.selectedSegmentIndex = 0 + codeModeSegmentedControl.setTitleTextAttributes( + [.foregroundColor: UIColor.white, .font: UIFont.systemFont(ofSize: 12)], + for: .normal) + codeModeSegmentedControl.setTitleTextAttributes( + [.foregroundColor: UIColor.black.withAlphaComponent(0.6), .font: UIFont.systemFont(ofSize: 12)], + for: .selected) + codeModeSegmentedControl.selectedSegmentIndex = 1 + codeModeSegmentedControl.tintColor = UIColor.white.withAlphaComponent(0.4) } private func configureTextView() { codeTextView.backgroundColor = .clear codeTextView.isEditable = false + codeTextView.tintColor = .gray } private func refreshViews() { - codeTextView.attributedText = viewModel?.getHighlightedText( - addViewControllerInCode: codeModeSegmentedControl.selectedSegmentIndex == 0 - ) + codeTextView.attributedText = viewModel?.getHighlightedText(type: selectedCodeType()) + codeTextView.contentInset = .init(top: 40 + view.safeAreaInsets.top, + left: 0, + bottom: 200 + view.safeAreaInsets.bottom, + right: 0) + codeTextView.contentOffset = .init(x: 0, y: -codeTextView.contentInset.top) + } + + private func selectedCodeType() -> LayoutDesignerCodePreviewViewModel.CodeType { + if codeModeSegmentedControl.selectedSegmentIndex == 0 { + return .uikit + } + if codeModeSegmentedControl.selectedSegmentIndex == 2 { + return .options + } + return .swiftui } - } @@ -107,7 +125,7 @@ extension LayoutDesignerCodePreviewViewController: UIDocumentPickerDelegate { controller.dismiss(animated: true) viewModel.removeSampleProject() } - + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { controller.dismiss(animated: true) viewModel.removeSampleProject() diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.xib b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.xib index ba00277..1d8671d 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.xib +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.xib @@ -1,9 +1,10 @@ - + - + + @@ -23,11 +24,11 @@ - - + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - + + - + - - + + + - + - + - + - - + + - @@ -97,5 +103,11 @@ + + + + + + diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift index e8d4e3b..ba86f68 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift @@ -10,37 +10,85 @@ import Foundation import Splash struct LayoutDesignerCodePreviewViewModel { - + + // MARK: Constant + + enum CodeType { + case uikit + case swiftui + case options + } + + // MARK: Properties let code: String var sampleProjectTempURL: URL? { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("SampleProject") } + + static private var fontSize: Double { + #if targetEnvironment(macCatalyst) + return 13 + #else + return 1_024 * 10 / 1_024 + #endif + } - private let highlighter = SyntaxHighlighter(format: AttributedStringOutputFormat(theme: .sundellsColors(withFont: Font(size: 14)))) - + private let highlighter = SyntaxHighlighter(format: AttributedStringOutputFormat(theme: Theme( + font: Font(size: Self.fontSize), + plainTextColor: .white, + tokenColors: [ + .keyword: Color(red: 1.00, green: 0.40, blue: 0.56, alpha: 1.00), + .string: Color(red: 0.98, green: 0.39, blue: 0.12, alpha: 1), + .type: Color(red: 0.57, green: 0.59, blue: 1.00, alpha: 1.00), + .call: Color(red: 0.2, green: 0.56, blue: 0.9, alpha: 1), + .number: Color(red: 0.97, green: 0.47, blue: 0.37, alpha: 1.00), + .comment: Color(red: 0.34, green: 0.72, blue: 0.80, alpha: 1.00), + .property: Color(red: 0.13, green: 0.67, blue: 0.62, alpha: 1), + .dotAccess: Color(red: 0.57, green: 0.7, blue: 0, alpha: 1), + .preprocessing: Color(red: 0.71, green: 0.54, blue: 0, alpha: 1) + ], + backgroundColor: .clear + ) + )) + // MARK: Public functions - func getHighlightedText(addViewControllerInCode includeVC: Bool) -> NSAttributedString { - highlighter.highlight(getCode(includeViewController: includeVC)) + func getHighlightedText(type: CodeType) -> NSAttributedString { + highlighter.highlight(getCode(type: type)) } - func generateSampleProject() { + func generateSampleProject(type: CodeType) { removeSampleProject() guard let sampleProjectTempURL = sampleProjectTempURL, - let bundlePath = Bundle.main.url(forResource: "SampleProject", withExtension: "bundle"), - let sampleProjectURL = Bundle(url: bundlePath)?.url(forResource: "SampleProject", withExtension: nil) else { - return + let bundlePath = Bundle.main.url(forResource: "SampleProject", withExtension: "bundle"), + let sampleProjectURL = Bundle(url: bundlePath)?.url(forResource: "SampleProject", withExtension: nil) else { + return } try? FileManager.default.copyItem(at: sampleProjectURL, to: sampleProjectTempURL) let baseProjectPath = sampleProjectTempURL.appendingPathComponent("PagingLayout") let viewControllerPath = baseProjectPath.appendingPathComponent("ViewController.swift") try? FileManager.default.removeItem(at: viewControllerPath) + + var type = type - let code = getCode(includeViewController: true) + if type == .options { + type = .swiftui + } + var code = getCode(type: type) + if type == .swiftui { + code.append(""" + + + func ViewController() -> UIViewController { + UIHostingController(rootView: ContentView()) + } + """) + } + try? code.write(to: viewControllerPath, atomically: true, encoding: .utf8) try? FileManager.default.moveItem(at: sampleProjectTempURL.appendingPathComponent("PagingLayout.xcodeproj_sample"), @@ -67,11 +115,69 @@ struct LayoutDesignerCodePreviewViewModel { // MARK: Private functions - - private func getCode(includeViewController: Bool) -> String { - if !includeViewController { + + private func getCode(type: CodeType) -> String { + switch type { + case .swiftui: + return getSwiftUICode() + case .uikit: + return getUIKitCode() + case .options: return code } + } + + private func getSwiftUICode() -> String { + let viewProtocols = ["ScaleTransformView", "StackTransformView", "SnapshotTransformView"] + let viewProtocolName = viewProtocols.first { code.contains($0) } ?? "" + let viewName = viewProtocolName.replacingOccurrences(of: "Transform", with: "Page") + return """ + import SwiftUI + + // Make sure you added this dependency to your project + // More info at https://bit.ly/CVPagingLayout + import CollectionViewPagingLayout + + struct ContentView: View { + + // Replace with your data + struct Item: Identifiable { + let id: UUID = .init() + let number: Int + } + let items = Array(0..<10).map { + Item(number: $0) + } + + // Use the options to customize the layout + \(code.replacingOccurrences(of: "scaleOptions", with: "options") + .replacingOccurrences(of: "stackOptions", with: "options") + .replacingOccurrences(of: "snapshotOptions", with: "options") + .replacingOccurrences(of: "\n", with: "\n ")) + + var body: some View { + \(viewName)(items) { item in + // Build your view here + ZStack { + Rectangle().fill(Color.orange) + Text("\\(item.number)") + } + } + .options(options) + // The padding around each page + // you can use `.fractionalWidth` and + // `.fractionalHeight` too + .pagePadding( + vertical: .absolute(100), + horizontal: .absolute(80) + ) + } + + } + """ + } + + private func getUIKitCode() -> String { let viewProtocols = ["ScaleTransformView", "StackTransformView", "SnapshotTransformView"] let viewProtocolName = viewProtocols.first { code.contains($0) } ?? "" @@ -88,7 +194,7 @@ struct LayoutDesignerCodePreviewViewModel { \(code.replacingOccurrences(of: "\n", with: "\n ")) - // The card view that we apply effects on + // The card view that we apply transforms on var card: UIView! override init(frame: CGRect) { @@ -102,13 +208,17 @@ struct LayoutDesignerCodePreviewViewModel { } func setup() { - // Adjust the card view frame you can use Autolayout too - let cardFrame = CGRect(x: 80, - y: 100, - width: frame.width - 160, - height: frame.height - 200) + + // Adjust the card view frame + // you can use Auto-layout too + let cardFrame = CGRect( + x: 80, + y: 100, + width: frame.width - 160, + height: frame.height - 200 + ) card = UIView(frame: cardFrame) - card.backgroundColor = .gray + card.backgroundColor = .systemOrange contentView.addSubview(card) } } @@ -125,25 +235,25 @@ struct LayoutDesignerCodePreviewViewModel { } private func setupCollectionView() { + let layout = CollectionViewPagingLayout() + collectionView = UICollectionView( frame: view.frame, - collectionViewLayout: CollectionViewPagingLayout() + collectionViewLayout: layout ) + collectionView.isPagingEnabled = true - collectionView.register(MyCell.self, forCellWithReuseIdentifier: "cell") + + collectionView.register( + MyCell.self, + forCellWithReuseIdentifier: "cell" + ) + collectionView.dataSource = self + view.addSubview(collectionView) } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - DispatchQueue.main.async { [weak self] in - self?.collectionView?.performBatchUpdates({ [weak self] in - self?.collectionView?.collectionViewLayout.invalidateLayout() - }) - } - } - func collectionView( _ collectionView: UICollectionView, numberOfItemsInSection section: Int @@ -153,7 +263,8 @@ struct LayoutDesignerCodePreviewViewModel { func collectionView( _ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { collectionView.dequeueReusableCell( withReuseIdentifier: "cell", for: indexPath diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift index 58babbd..c85f585 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift @@ -34,15 +34,6 @@ class LayoutDesignerIntroViewController: UIViewController { open() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - DispatchQueue.main.async { [weak self] in - self?.collectionView?.performBatchUpdates({ [weak self] in - self?.collectionView?.collectionViewLayout.invalidateLayout() - }) - } - } - // MARK: Private functions diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift index 1f453eb..9fcc358 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift @@ -39,17 +39,9 @@ class LayoutDesignerViewController: UIViewController, ViewModelBased, NibBased { configureViews() setOptionsList() registerKeyboardNotifications() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if viewModel.shouldShowIntro { - showIntroViewController(viewModel: viewModel.getIntroViewModel()) - } - } - - deinit { - NotificationCenter.default.removeObserver(self) + + view.backgroundColor = .clear + codeContainerView.backgroundColor = codeContainerView.backgroundColor?.withAlphaComponent(0.6) } diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib index 739fe8f..e3ec631 100644 --- a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib @@ -1,10 +1,11 @@ - + - + + @@ -25,7 +26,7 @@ - + @@ -35,33 +36,33 @@ - + - + - + - - + + - + - + @@ -70,7 +71,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + - + - - - - - - + - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + - - - + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + - + - - - - - - + - - - - - - - - - + + - + + - + + + - - - - + + - - + - + + @@ -434,5 +643,9 @@ + + + + diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/DevicesView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/DevicesView.swift new file mode 100644 index 0000000..1961a6c --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/DevicesView.swift @@ -0,0 +1,133 @@ +// +// DevicesView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 30/01/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +// Design by Cuberto https://dribbble.com/shots/12580831-Principle-Tutorial-Onboarding-Flow-Animation + +import SwiftUI +import CollectionViewPagingLayout + +struct Device: Identifiable { + let name: String + let iconName: String + let color: Color + + var id: String { + name + } +} + +struct DevicesView: View { + + private let devices: [Device] = [ + Device(name: "iPad", iconName: "ipad", color: .yellow), + Device(name: "Apple Watch", iconName: "applewatch", color: .green), + Device(name: "iPhone", iconName: "iphone", color: .orange), + Device(name: "AirPods Pro", iconName: "airpodspro", color: .blue), + Device(name: "Mac Pro", iconName: "macpro.gen3", color: .red), + Device(name: "HomePod", iconName: "homepod", color: .purple) + ] + + private let scaleFactor: CGFloat = 130 + private let circleSize: CGFloat = 80 + + @State private var currentDeviceName: String? + + var body: some View { + TransformPageView(devices, selection: $currentDeviceName) { device, progress in + ZStack { + roundedRectangle(device: device, progress: progress) + deviceView(device: device, progress: progress) + HStack { + Image(systemName: "chevron.right") + } + .foregroundColor(.white) + .font(.system(size: 30)) + .transformEffect(.init(translationX: -300 * (progress - 1), y: 0)) + .padding(.top, 400) + .opacity(1 - Double(abs(progress - 1))) + } + } + .animator(DefaultViewAnimator(0.7, curve: .parametric)) + .scrollToSelectedPage(false) + .onTapPage { name in + currentDeviceName = currentDeviceName == devices.last?.name ? devices.first?.name : name + } + .zPosition(zPosition) + .collectionView(\.showsHorizontalScrollIndicator, false) + .ignoresSafeArea() + } + + private func deviceView(device: Device, progress: CGFloat) -> some View { + VStack { + Image(systemName: device.iconName) + .font(.system(size: 160)) + Text(device.name) + .font(.system(size: 40)) + .padding(.top, 10) + Spacer() + .frame(maxHeight: 200) + } + .frame(maxHeight: .infinity) + .foregroundColor(.white) + .transformEffect(.init(translationX: 400 * progress, y: 0)) + } + + private func roundedRectangle(device: Device, progress: CGFloat) -> some View { + let scale = getScale(progress) + return RoundedRectangle(cornerRadius: circleSize * ((0.2 * scaleFactor) / scale)) + .fill() + .frame(width: circleSize, height: circleSize) + .scaleEffect(scale, anchor: scaleAnchor(progress)) + .transformEffect(.init(translationX: translationX(progress), y: 0)) + .padding(.top, 400) + .foregroundColor(device.color) + .opacity((1.25 - max(1, abs(Double(progress)))) / 0.25) + } + + private func translationX(_ progress: CGFloat) -> CGFloat { + guard progress >= 1 || progress < -0.5 else { return 0 } + return -2 * (progress + (progress > 0 ? -1 : 1)) * circleSize + } + + private func zPosition(_ progress: CGFloat) -> Int { + if progress < -1 { return 3 } + if progress < 0 { return 2 } + if progress < 0.5 { return 1 } + if progress <= 1 { return 4 } + if progress < 1.5 { return 2 } + return -1 + } + + private func getScale(_ progress: CGFloat) -> CGFloat { + var scale: CGFloat = progress > 1 ? progress - 1 : 1 - progress + if progress <= -1 { + scale = -progress - 1 + } else if progress < -0.5 { + scale = progress + 1 + } else if progress <= 0.5 { + scale = scaleFactor + } + return 1 + scale * scaleFactor + } + + private func scaleAnchor(_ progress: CGFloat) -> UnitPoint { + if progress <= -1 { return .leading } + if progress <= -0.5 { return .trailing } + if progress < 0.5 { return .center } + if progress < 1 { return .leading } + return .trailing + } +} + + +struct DevicesView_Previews: PreviewProvider { + static var previews: some View { + DevicesView() + .ignoresSafeArea() + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.ShapeView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.ShapeView.swift new file mode 100644 index 0000000..4757243 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.ShapeView.swift @@ -0,0 +1,50 @@ +// +// ShapesListView.ShapeView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 17/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI + +extension ShapesListView { + struct ShapeView: View { + let shape: Shape + let color: LinearGradient + + var body: some View { + ZStack { + color + .border(Color.white, width: 6) + VStack { + Image(systemName: shape.iconName) + .font(.system(size: 42)) + .padding(.bottom, 10) + Text(shape.name) + .font(.title2) + Image("textPlaceholder") + .resizable() + .scaledToFit() + .frame(maxWidth: 90) + .padding(.horizontal, 20) + } + } + .foregroundColor(.white) + } + } +} + +struct ShapesListView_ShapeView_Previews: PreviewProvider { + static var previews: some View { + ShapesListView.ShapeView( + shape: .init(name: "Hexagon", iconName: "hexagon.fill"), + color: LinearGradient( + gradient: .init(colors: [.red, .black]), + startPoint: .topLeading, + endPoint: .bottomLeading + ) + ) + .previewLayout(.fixed(width: 190, height: 300)) + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.swift new file mode 100644 index 0000000..4cd41da --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/Shapes/ShapesListView.swift @@ -0,0 +1,149 @@ +// +// ShapesListView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 17/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI +import CollectionViewPagingLayout + +struct ShapesListView: View { + + let layoutGroup: LayoutGroup + + let shapes: [Shape] = [ + .init(name: "Hexagon", iconName: "hexagon.fill"), + .init(name: "Rectangle", iconName: "rectangle.fill"), + .init(name: "Shield", iconName: "shield.fill"), + .init(name: "App", iconName: "app.fill"), + .init(name: "Triangle", iconName: "triangle.fill"), + .init(name: "Circle", iconName: "circle.fill"), + .init(name: "Square", iconName: "square.fill"), + .init(name: "Capsule", iconName: "capsule.fill") + ] + + var scaleGradient: LinearGradient { + LinearGradient(gradient: .init(colors: [Color("pastelRed"), Color("supernova")]), + startPoint: .topLeading, endPoint: .bottomTrailing) + } + + var stackGradient: LinearGradient { + LinearGradient(gradient: .init(colors: [Color("babyBlue"), Color("matisse")]), + startPoint: .topLeading, endPoint: .bottomTrailing) + } + + var snapshotGradient: LinearGradient { + LinearGradient(gradient: .init(colors: [Color("chartreuseYellow"), Color("mayaBlue")]), + startPoint: .topLeading, endPoint: .bottomTrailing) + } + + var body: some View { + VStack { + Text("S\(String(layoutGroup.rawValue.dropFirst()))PageView") + .font(.title2) + .fontWeight(.bold) + .padding(.top, 19) + + ScrollView { + switch layoutGroup { + case .stack: + stackLayouts() + case .scale: + scaleLayouts() + case .snapshot: + snapshotLayouts() + } + } + .frame(maxWidth: .infinity) + } + } + + func stackLayouts() -> some View { + ForEach(StackTransformViewOptions.Layout.allCases) { layout in + VStack(alignment: .leading) { + layoutTitle(layout.rawValue) + StackPageView(shapes) { shape in + ShapeView(shape: shape, color: stackGradient) + } + .options(.layout(layout)) + .pagePadding(vertical: .fractionalHeight(0.1), + horizontal: .fractionalWidth(0.3)) + .frame(height: 300) + .padding(10) + .background(Color("Background")) + .cornerRadius(26) + } + .padding(19) + } + } + + func scaleLayouts() -> some View { + ForEach(ScaleTransformViewOptions.Layout.allCases) { layout in + VStack(alignment: .leading) { + layoutTitle(layout.rawValue) + ScalePageView(shapes) { shape in + ShapeView(shape: shape, color: scaleGradient) + } + .options(.layout(layout)) + .pagePadding(vertical: .fractionalHeight(0.1), + horizontal: .fractionalWidth(0.3)) + .frame(height: 300) + .padding(10) + .background(Color("Background")) + .cornerRadius(26) + } + .padding(19) + } + } + + func snapshotLayouts() -> some View { + ForEach(SnapshotTransformViewOptions.Layout.allCases) { layout in + VStack(alignment: .leading) { + layoutTitle(layout.rawValue) + SnapshotPageView(shapes) { shape in + ShapeView(shape: shape, color: snapshotGradient) + } + .options(.layout(layout)) + .pagePadding(vertical: .fractionalHeight(0.1), + horizontal: .fractionalWidth(0.3)) + .frame(height: 300) + .padding(10) + .background(Color("Background")) + .cornerRadius(26) + } + .padding(19) + } + } + + func layoutTitle(_ title: String) -> some View { + Text(".\(title)") + .fontWeight(.semibold) + .foregroundColor(.gray) + .padding(.bottom, 8) + } +} + +extension StackTransformViewOptions.Layout: Identifiable { + public var id: String { + rawValue + } +} +extension ScaleTransformViewOptions.Layout: Identifiable { + public var id: String { + rawValue + } +} +extension SnapshotTransformViewOptions.Layout: Identifiable { + public var id: String { + rawValue + } +} + + +extension ShapesListView { + enum LayoutGroup: String { + case stack, scale, snapshot + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherPage.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherPage.swift new file mode 100644 index 0000000..91f0ceb --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherPage.swift @@ -0,0 +1,141 @@ +// +// WeatherPage.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 14/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import Foundation + +enum WeatherPage: String, CaseIterable, Identifiable { + case sun + case lightning + case tornado + case moon + case snow + + var id: String { + rawValue + } + + var imageName: String { + switch self { + case .sun: + return "sun.max.fill" + case .lightning: + return "cloud.bolt.rain.fill" + case .moon: + return "moon.stars.fill" + case .tornado: + return "tornado" + case .snow: + return "snow" + } + } + + var name: String { + rawValue.prefix(1).capitalized + rawValue.dropFirst() + } +} + +extension WeatherPage { + struct Content { + let items: [Item] + + enum Item { + case text(String) + case image(String) + } + } +} + +// swiftlint:disable line_length +extension WeatherPage { + var content: Content { + switch self { + case .sun: + return .init(items: [ + .text( + """ + The Sun is the star at the center of the Solar System. It is a nearly perfect sphere of hot plasma, heated to incandescence by nuclear fusion reactions in its core, radiating the energy mainly as visible light and infrared radiation. + It is by far the most important source of energy for life on Earth. + Its diameter is about 1.39 million kilometres (864,000 miles), or 109 times that of Earth. + Its mass is about 330,000 times that of Earth, and accounts for about 99.86% of the total mass of the Solar System. + """ + ), + .image("sun1"), + .text( + """ + The Sun is a G-type main-sequence star (G2V) based on its spectral class. As such, it is informally and not completely accurately referred to as a yellow dwarf (its light is closer to white than yellow). + It formed approximately 4.6 billion years ago from the gravitational collapse of matter within a region of a large molecular cloud. + """ + ), + .image("sun2") + ]) + case .lightning: + return .init(items: [ + .text( + """ + Lightning is a naturally occurring electrostatic discharge during which two electrically charged regions in the atmosphere or ground temporarily equalize themselves, causing the instantaneous release of as much as one gigajoule of energy. + This discharge may produce a wide range of electromagnetic radiation, from very hot plasma created by the rapid movement of electrons to brilliant flashes of visible light in the form of black-body radiation. Lightning causes thunder, a sound from the shock wave which develops as gases in the vicinity of the discharge experience a sudden increase in pressure. + """ + ), + .image("lightning1"), + .text( + """ + The three main kinds of lightning are distinguished by where they occur: either inside a single thundercloud, between two different clouds, or between a cloud and the ground. + Many other observational variants are recognized, including "heat lightning", which can be seen from a great distance but not heard; dry lightning, which can cause forest fires; and ball lightning, which is rarely observed scientifically. + """ + ), + .image("lightning2") + ]) + case .tornado: + return .init(items: [ + .text( + """ + A tornado is a violently rotating column of air that is in contact with both the surface of the Earth and a cumulonimbus cloud or, in rare cases, the base of a cumulus cloud. + The windstorm is often referred to as a twister, whirlwind or cyclone, although the word cyclone is used in meteorology to name a weather system with a low-pressure area in the center around which, from an observer looking down toward the surface of the earth, winds blow counterclockwise in the Northern Hemisphere and clockwise in the Southern. + """ + ), + .image("tornado1"), + .text( + """ + Various types of tornadoes include the multiple vortex tornado, landspout, and waterspout. Waterspouts are characterized by a spiraling funnel-shaped wind current, connecting to a large cumulus or cumulonimbus cloud. They are generally classified as non-supercellular tornadoes that develop over bodies of water, but there is disagreement over whether to classify them as true tornadoes. + """ + ), + .image("tornado2") + ]) + case .moon: + return .init(items: [ + .text( + """ + The Moon is Earth's only proper natural satellite. At one-quarter the diameter of Earth (comparable to the width of Australia), it is the largest natural satellite in the Solar System relative to the size of its planet, and the fifth largest satellite in the Solar System overall (larger than any dwarf planet). + """ + ), + .image("moon1"), + .text( + """ + The Moon's orbit around Earth has a sidereal period of 27.3 days, and a synodic period of 29.5 days. The synodic period drives its lunar phases, which form the basis for the months of a lunar calendar. The Moon is tidally locked to Earth, which means that the length of a full rotation of the Moon on its own axis (a lunar day) is the same as the synodic period, resulting in its same side (the near side) always facing Earth. That said, 59% of the total lunar surface can be seen from Earth through shifts in perspective (its libration). + """ + ), + .image("moon2") + ]) + case .snow: + return .init(items: [ + .text( + """ + Snow comprises individual ice crystals that grow while suspended in the atmosphere—usually within clouds—and then fall, accumulating on the ground where they undergo further changes.[2] It consists of frozen crystalline water throughout its life cycle, starting when, under suitable conditions, the ice crystals form in the atmosphere, increase to millimeter size, precipitate and accumulate on surfaces, then metamorphose in place, and ultimately melt, slide or sublimate away. + """ + ), + .image("snow1"), + .text( + """ + Major snow-prone areas include the polar regions, the northernmost half of the Northern Hemisphere and mountainous regions worldwide with sufficient moisture and cold temperatures. In the Southern Hemisphere, snow is confined primarily to mountainous areas, apart from Antarctica. + """ + ), + .image("snow2") + ]) + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.Overlay.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.Overlay.swift new file mode 100644 index 0000000..124c067 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.Overlay.swift @@ -0,0 +1,44 @@ +// +// WeatherTabView.Overlay.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 14/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI + +extension WeatherTabView { + struct Overlay: View { + var body: some View { + VStack { + LinearGradient( + gradient: Gradient( + colors: [ + Color("Background"), + Color("Background").opacity(0) + ] + ), + startPoint: .top, + endPoint: .bottom + ) + .frame(maxWidth: .infinity, maxHeight: 30) + .padding(.top, 50) + + Spacer() + + LinearGradient( + gradient: Gradient( + colors: [ + Color("Background").opacity(0), + Color("Background") + ] + ), + startPoint: .top, + endPoint: .bottom + ) + .frame(maxWidth: .infinity, maxHeight: 40) + } + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.PageView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.PageView.swift new file mode 100644 index 0000000..f7b5f22 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.PageView.swift @@ -0,0 +1,49 @@ +// +// WeatherTabView.PageView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 15/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI + +extension WeatherTabView { + struct PageView: View { + let page: WeatherPage + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Image("WikipediaLogo") + .opacity(0.34) + Text(page.name) + .font(.system(size: 53, weight: .bold, design: .serif)) + ForEach(page.content.items) { item in + switch item { + case .image(let imageName): + Image(imageName) + .resizable() + .scaledToFit() + .border(Color.white, width: 11) + case .text(let text): + Text(text) + .font(.system(size: 16, weight: .regular, design: .default)) + .foregroundColor(.gray) + } + } + } + .padding(.bottom, 100) + .padding(.top, 20) + .padding(.horizontal, 34) + } + } +} + +extension WeatherPage.Content.Item: Identifiable { + var id: String { + switch self { + case .image(let data), .text(let data): + return data.hash.description + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.TabView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.TabView.swift new file mode 100644 index 0000000..849da3a --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.TabView.swift @@ -0,0 +1,84 @@ +// +// WeatherTabView.TabView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 14/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI + +extension WeatherTabView { + struct TabView: View { + @Binding var selection: WeatherPage.ID? + + var body: some View { + VStack { + Spacer() + ZStack(alignment: .center) { + VisualEffectView(style: .systemUltraThinMaterialDark) + buttons + } + .cornerRadius(21) + .frame(height: 72) + .padding(.horizontal, 23) + .padding(.bottom, 20) + .animation(.easeInOut) + } + } + + private var buttons: some View { + HStack(spacing: 0) { + Spacer(minLength: 0) + .frame(maxWidth: isSelected(WeatherPage.allCases.first) ? 12 : .infinity) + + ForEach(WeatherPage.allCases) { page in + Button { + selection = page.id + } label: { + HStack { + Spacer(minLength: isSelected(page) ? 12 : 0) + Image(systemName: page.imageName) + .font(.system(size: 25)) + if isSelected(page) { + Text(page.name) + .font(.system(size: 18, weight: .light)) + .lineLimit(1) + .fixedSize() + } + Spacer(minLength: isSelected(page) ? 12 : 0) + } + .padding(.vertical, 9) + .background( + isSelected(page) ? Color.black.opacity(0.15) : Color.clear + ) + .cornerRadius(17) + } + Spacer(minLength: 0) + .frame(maxWidth: page == WeatherPage.allCases.last && isSelected(WeatherPage.allCases.last) ? 12 : .infinity) + } + } + .foregroundColor(.white) + } + + private func isSelected(_ page: WeatherPage?) -> Bool { + page?.id == selection ?? WeatherPage.allCases.first?.id + } + + } +} + +struct WeatherTabView_TabView_Previews: PreviewProvider { + static var previews: some View { + Group { + WeatherTabView.TabView(selection: .constant(WeatherPage.sun.id)) + .previewLayout(.fixed(width: 450, height: 300)) + + WeatherTabView.TabView(selection: .constant(WeatherPage.tornado.id)) + .previewLayout(.fixed(width: 375, height: 300)) + + WeatherTabView.TabView(selection: .constant(WeatherPage.lightning.id)) + .previewLayout(.fixed(width: 320, height: 300)) + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.swift b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.swift new file mode 100644 index 0000000..f77bf72 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/SwiftUI/WeatherTabView/WeatherTabView.swift @@ -0,0 +1,42 @@ +// +// WeatherTabView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 13/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI +import CollectionViewPagingLayout + +struct WeatherTabView: View { + + @State private var currentPage: WeatherPage.ID? + + var body: some View { + ZStack { + Color("Background").ignoresSafeArea() + + SnapshotPageView(WeatherPage.allCases, selection: $currentPage) { page in + ScrollView(showsIndicators: false) { + PageView(page: page) + } + } + .animator(DefaultViewAnimator(0.7, curve: .parametric)) + .options(options) + .padding(.top, 50) + .overlay(Overlay()) + + TabView(selection: $currentPage) + } + } + + private var options = SnapshotTransformViewOptions( + pieceSizeRatio: .init(width: 0.2, height: 1), + piecesAlphaRatio: .static(0), + piecesTranslationRatio: .columnOddEven(CGPoint(x: 0, y: 0.1), CGPoint(x: 0, y: -0.1)), + piecesScaleRatio: .columnOddEven(.init(width: 1, height: 0), .init(width: 0, height: 0)), + containerScaleRatio: 0, + containerTranslationRatio: .init(x: 1, y: 0) + ) +} diff --git a/Samples/PagingLayoutSamples/Modules/Cards/CardsViewController.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewController.swift similarity index 91% rename from Samples/PagingLayoutSamples/Modules/Cards/CardsViewController.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewController.swift index 1ef0eef..a5ca682 100644 --- a/Samples/PagingLayoutSamples/Modules/Cards/CardsViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewController.swift @@ -44,10 +44,12 @@ class CardsViewController: UIViewController, NibBased, ViewModelBased { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - layout.invalidateLayout() + layout.invalidateLayoutInBatchUpdate() if !didScrollCollectionViewToMiddle { - layout.setCurrentPage(Constants.infiniteNumberOfItems / 2, animated: false) didScrollCollectionViewToMiddle = true + collectionView?.performBatchUpdates({ [weak self] in + self?.layout.setCurrentPage(Constants.infiniteNumberOfItems / 2, animated: false) + }) } } @@ -81,6 +83,7 @@ class CardsViewController: UIViewController, NibBased, ViewModelBased { collectionView.dataSource = self layout.numberOfVisibleItems = 7 layout.scrollDirection = .vertical + layout.transparentAttributeWhenCellNotLoaded = true collectionView.collectionViewLayout = layout collectionView.showsVerticalScrollIndicator = false collectionView.clipsToBounds = false diff --git a/Samples/PagingLayoutSamples/Modules/Cards/CardsViewController.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewController.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Cards/CardsViewController.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewController.xib diff --git a/Samples/PagingLayoutSamples/Modules/Cards/CardsViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Cards/CardsViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/CardsViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCellViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCellViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCellViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCellViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCollectionViewCell.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCollectionViewCell.swift diff --git a/Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCollectionViewCell.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCollectionViewCell.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Cards/Cell/CardCollectionViewCell.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Cards/Cell/CardCollectionViewCell.xib diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitCellViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitCellViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitCellViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitCellViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.swift diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.xib diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewController.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewController.swift similarity index 93% rename from Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewController.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewController.swift index 11cadff..e5b0ef9 100644 --- a/Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewController.swift @@ -27,11 +27,6 @@ class FruitsViewController: UIViewController, NibBased, ViewModelBased { configureViews() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - collectionView.collectionViewLayout.invalidateLayout() - } - // MARK: Event listener @IBAction private func onBackTouched() { diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewController.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewController.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewController.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewController.xib diff --git a/Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Fruits/FruitsViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/FruitsViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCellViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCellViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCellViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCellViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCollectionViewCell.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCollectionViewCell.swift diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCollectionViewCell.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCollectionViewCell.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Gallery/Cell/PhotoCollectionViewCell.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/Cell/PhotoCollectionViewCell.xib diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewController.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewController.swift similarity index 93% rename from Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewController.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewController.swift index 0106769..5544c23 100644 --- a/Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewController.swift @@ -33,11 +33,6 @@ class GalleryViewController: UIViewController, NibBased, ViewModelBased { configureViews() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - collectionView.collectionViewLayout.invalidateLayout() - } - // MARK: Event listener diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewController.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewController.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewController.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewController.xib diff --git a/Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Gallery/GalleryViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Gallery/GalleryViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardView.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardView.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardView.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardView.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardView.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardView.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardView.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardView.xib diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/Card/ShapeCardViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/Card/ShapeCardViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.xib diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/ShapeCollectionViewCells.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/ShapeCollectionViewCells.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/ShapeCollectionViewCells.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/ShapeCollectionViewCells.swift diff --git a/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+ScaleOptions.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+ScaleOptions.swift new file mode 100644 index 0000000..ff17a37 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+ScaleOptions.swift @@ -0,0 +1,35 @@ +// +// ShapeLayout+ScaleOptions.swift +// PagingLayoutSamples +// +// Created by Amir on 27/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import CollectionViewPagingLayout + +extension ShapeLayout { + var scaleOptions: ScaleTransformViewOptions? { + switch self { + case .scaleBlur: + return .layout(.blur) + case .scaleLinear: + return .layout(.linear) + case .scaleEaseIn: + return .layout(.easeIn) + case .scaleEaseOut: + return .layout(.easeOut) + case .scaleRotary: + return .layout(.rotary) + case .scaleCylinder: + return .layout(.cylinder) + case .scaleInvertedCylinder: + return .layout(.invertedCylinder) + case .scaleCoverFlow: + return .layout(.coverFlow) + default: + return nil + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+SnapshotOptions.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+SnapshotOptions.swift new file mode 100644 index 0000000..23a7e8a --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+SnapshotOptions.swift @@ -0,0 +1,36 @@ +// +// ShapeLayout+SnapshotOptions.swift +// PagingLayoutSamples +// +// Created by Amir on 27/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import CollectionViewPagingLayout + +extension ShapeLayout { + var snapshotOptions: SnapshotTransformViewOptions? { + switch self { + case .snapshotGrid: + return .layout(.grid) + case .snapshotSpace: + return .layout(.space) + case .snapshotChess: + return .layout(.chess) + case .snapshotTiles: + return .layout(.tiles) + case .snapshotLines: + return .layout(.lines) + case .snapshotBars: + return .layout(.bars) + case .snapshotPuzzle: + return .layout(.puzzle) + case .snapshotFade: + return .layout(.fade) + default: + return nil + } + + } +} diff --git a/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+StackOptions.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+StackOptions.swift new file mode 100644 index 0000000..9fb5cec --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeLayout+StackOptions.swift @@ -0,0 +1,31 @@ +// +// ShapeLayout+StackOptions.swift +// PagingLayoutSamples +// +// Created by Amir on 27/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import CollectionViewPagingLayout + +extension ShapeLayout { + var stackOptions: StackTransformViewOptions? { + switch self { + case .stackTransparent: + return .layout(.transparent) + case .stackPerspective: + return .layout(.perspective) + case .stackRotary: + return .layout(.rotary) + case .stackVortex: + return .layout(.vortex) + case .stackReverse: + return .layout(.reverse) + case .stackBlur: + return .layout(.blur) + default: + return nil + } + } +} diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.swift similarity index 89% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.swift index c68511a..8a07842 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.swift @@ -20,7 +20,7 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { private struct Constants { - static let infiniteNumberOfItems = 100_000 + static let infiniteNumberOfItems = 10_000 } @@ -54,15 +54,17 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { super.viewDidLayoutSubviews() if !didScrollCollectionViewToMiddle { - getPagingLayout(layoutTypeCollectionView)?.setCurrentPage( - Constants.infiniteNumberOfItems / 2, - animated: false - ) didScrollCollectionViewToMiddle = true + collectionView?.performBatchUpdates({ [weak self] in + self?.getPagingLayout(layoutTypeCollectionView)?.setCurrentPage( + Constants.infiniteNumberOfItems / 2, + animated: false + ) + }) } updateSelectedLayout() - invalidateLayouts() + getPagingLayout(layoutTypeCollectionView)?.invalidateLayoutInBatchUpdate() } @@ -111,13 +113,12 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { collectionView.isPagingEnabled = true collectionView.dataSource = self let layout = CollectionViewPagingLayout() - layout.numberOfVisibleItems = 10 collectionView.collectionViewLayout = layout - layout.configureTapOnCollectionView(goToSelectedPage: true) layout.delegate = self collectionView.showsHorizontalScrollIndicator = false collectionView.clipsToBounds = false collectionView.backgroundColor = .clear + collectionView.delegate = self } private func configureLayoutTypeCollectionView() { @@ -127,7 +128,6 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { let layout = CollectionViewPagingLayout() layout.numberOfVisibleItems = 10 layoutTypeCollectionView.collectionViewLayout = layout - layout.configureTapOnCollectionView(goToSelectedPage: true) layoutTypeCollectionView.showsHorizontalScrollIndicator = false layoutTypeCollectionView.clipsToBounds = false layoutTypeCollectionView.backgroundColor = .clear @@ -135,9 +135,7 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { } private func updateSelectedLayout() { - guard let layout = layoutTypeCollectionView?.collectionViewLayout as? CollectionViewPagingLayout else { - return - } + guard let layout = getPagingLayout(layoutTypeCollectionView) else { return } let index = layout.currentPage % viewModel.layoutTypeViewModels.count self.viewModel.selectedLayout = self.viewModel.layoutTypeViewModels[index] delegate?.shapesViewController(self, onSelectedLayoutChange: viewModel.selectedLayout.layout) @@ -167,8 +165,8 @@ class ShapesViewController: UIViewController, NibBased, ViewModelBased { }) } - private func getPagingLayout(_ collectionView: UICollectionView) -> CollectionViewPagingLayout? { - collectionView.collectionViewLayout as? CollectionViewPagingLayout + private func getPagingLayout(_ collectionView: UICollectionView?) -> CollectionViewPagingLayout? { + collectionView?.collectionViewLayout as? CollectionViewPagingLayout } } @@ -242,6 +240,14 @@ extension ShapesViewController: UICollectionViewDelegate { } updateSelectedLayout() } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.setCurrentPage(indexPath.row) { [weak self] in + DispatchQueue.main.async { + self?.updateSelectedLayout() + } + } + } } diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.xib similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.xib diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewModel.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewModel.swift similarity index 100% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewModel.swift diff --git a/Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject/PagingLayout.xcodeproj_sample/project.pbxproj_sample b/Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject/PagingLayout.xcodeproj_sample/project.pbxproj_sample index 088a211..3aad101 100644 --- a/Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject/PagingLayout.xcodeproj_sample/project.pbxproj_sample +++ b/Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject/PagingLayout.xcodeproj_sample/project.pbxproj_sample @@ -339,7 +339,7 @@ repositoryURL = "https://github.com/amirdew/CollectionViewPagingLayout"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.3.0; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Samples/PagingLayoutSamples/Utilities/VisualEffectView.swift b/Samples/PagingLayoutSamples/Utilities/VisualEffectView.swift new file mode 100644 index 0000000..f0cfa54 --- /dev/null +++ b/Samples/PagingLayoutSamples/Utilities/VisualEffectView.swift @@ -0,0 +1,22 @@ +// +// VisualEffectView.swift +// PagingLayoutSamples +// +// Created by Amir Khorsandi on 14/04/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import SwiftUI +import UIKit + +struct VisualEffectView: UIViewRepresentable { + var style: UIBlurEffect.Style = .systemUltraThinMaterial + + func makeUIView(context: Context) -> UIVisualEffectView { + let view = UIVisualEffectView(effect: UIBlurEffect(style: style)) + return view + } + func updateUIView(_ uiView: UIVisualEffectView, context: Context) { + uiView.effect = UIBlurEffect(style: style) + } +} diff --git a/Samples/Podfile b/Samples/Podfile index bb8f143..164b4d3 100644 --- a/Samples/Podfile +++ b/Samples/Podfile @@ -1,4 +1,4 @@ -platform :ios, '13.0' +platform :ios, '14.0' target 'PagingLayoutSamples' do use_frameworks! diff --git a/Samples/Podfile.lock b/Samples/Podfile.lock index 58b4d74..6bb5023 100644 --- a/Samples/Podfile.lock +++ b/Samples/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - CollectionViewPagingLayout (0.3.0) + - CollectionViewPagingLayout (1.1.0) - Splash (0.13.0) - - SwiftLint (0.39.2) + - SwiftLint (0.47.0) DEPENDENCIES: - CollectionViewPagingLayout (from `./../`) @@ -20,14 +20,14 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Splash: - :commit: 3ed14d500c85663d35136855e7ce4ef064288db2 + :commit: 919ad99dfdc16b82665c44c3b6494f4e5f22bcec :git: https://github.com/amirdew/Splash.git SPEC CHECKSUMS: - CollectionViewPagingLayout: 7c2d6f975d467356e2ddfd72aef8534f1ebd946d + CollectionViewPagingLayout: 2f8d50683bae57ca88f47fa4ab0195c9079582cf Splash: 5ec9a07c4bbf047ddd659bfc11d35331d31004f7 - SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 + SwiftLint: d41cc46a2ae58ac6d9f26954bc89f1d72e71fdef -PODFILE CHECKSUM: ba360cc7c61c7efa5897648fc2987642be2e493e +PODFILE CHECKSUM: a829b1accd80b68e4033b32927c85146318f7597 -COCOAPODS: 1.9.3 +COCOAPODS: 1.11.3