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 fd9bf12..9221a3b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -37,6 +37,10 @@ opt_in_rules: excluded: - Package.swift - Pods + - PagingLayoutSamples/SampleProject.bundle/SampleProject + - Samples/Pods + - Samples/PagingLayoutSamples/SampleProject.bundle/SampleProject + - CollectionViewPagingLayoutTests # adjusting rules @@ -51,18 +55,12 @@ file_header: required_pattern: | \/\/ \/\/ .*?\.swift - \/\/ (CollectionViewPagingLayout.*?|Unit Tests) + \/\/ .* \/\/ \/\/ (Created by .*? on .*?) \/\/ 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 ff80091..f081bb6 100644 --- a/CollectionViewPagingLayout.podspec +++ b/CollectionViewPagingLayout.podspec @@ -1,21 +1,23 @@ - Pod::Spec.new do |s| - s.name = "CollectionViewPagingLayout" - s.version = "0.1.5" - 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 b917338..f9f1b86 100644 --- a/Lib/CollectionViewPagingLayout.swift +++ b/Lib/CollectionViewPagingLayout.swift @@ -8,25 +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) } +public extension CollectionViewPagingLayoutDelegate { + func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) {} +} + + 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) @@ -43,33 +74,79 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { } private var numberOfItems: Int { - (0..<(collectionView?.numberOfSections ?? 0)) + guard let numberOfSections = collectionView?.numberOfSections, numberOfSections > 0 else { + return 0 + } + return (0.. Void)? = nil) { + safelySetCurrentPage(page, animated: animated, animator: animator, completion: completion) + } + + public func setCurrentPage(_ page: Int, + animated: Bool = true, + completion: (() -> Void)? = nil) { + safelySetCurrentPage(page, animated: animated, animator: defaultAnimator, completion: completion) } - public func goToNextPage(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 goToPreviousPage(animated: Bool = true) { - setCurrentPage(currentPage - 1, animated: animated) + 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() + }) + } } // MARK: UICollectionViewLayout override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - true + if newBounds.size != visibleRect.size { + currentPageCache = currentPage + } + return true } - + override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let currentScrollOffset = self.currentScrollOffset let numberOfItems = self.numberOfItems @@ -90,7 +167,7 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { let startIndex = max(0, initialStartIndex - endIndexOutOfBounds) let endIndex = min(numberOfItems, initialEndIndex + startIndexOutOfBounds) - var attributesArray: [UICollectionViewLayoutAttributes] = [] + var attributesArray: [(page: Int, attributes: UICollectionViewLayoutAttributes)] = [] var section = 0 var numberOfItemsInSection = collectionView?.numberOfItems(inSection: section) ?? 0 var numberOfItemsInPrevSections = 0 @@ -117,28 +194,46 @@ 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) } - - cellAttributes.zIndex = zIndex - attributesArray.append(cellAttributes) + + // 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 + } + attributesArray.append((page: Int(pageIndex), attributes: cellAttributes)) } - return attributesArray + attributesCache = attributesArray + addBoundsObserverIfNeeded() + return attributesArray.map(\.attributes) } override public func invalidateLayout() { super.invalidateLayout() - updateCurrentPageIfNeeded() + if let page = currentPageCache { + setCurrentPage(page, animated: false) + currentPageCache = nil + } else { + updateCurrentPageIfNeeded() + } } // MARK: Private functions - private func updateCurrentPageIfNeeded(basedOn contentOffset: CGPoint? = nil) { + private func updateCurrentPageIfNeeded() { var currentPage: Int = 0 if let collectionView = collectionView { - let contentOffset = contentOffset ?? collectionView.contentOffset + let contentOffset = collectionView.contentOffset let pageSize = scrollDirection == .horizontal ? collectionView.frame.width : collectionView.frame.height let offset = scrollDirection == .horizontal ? (contentOffset.x + collectionView.contentInset.left) : @@ -147,7 +242,7 @@ public class CollectionViewPagingLayout: UICollectionViewLayout { currentPage = Int(round(offset / pageSize)) } } - if currentPage != self.currentPage { + if currentPage != self.currentPage, !isAnimating { self.currentPage = currentPage } } @@ -166,15 +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) - collectionView?.setContentOffset(contentOffset, animated: animated) - updateCurrentPageIfNeeded(basedOn: contentOffset) + + 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) + } + + // this is necessary when we want to set the current page without animation + if !animated, page != currentPage { + invalidateLayoutInBatchUpdate() + } + } + + 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?() + } + } + } +} + + +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 9ea1e3b..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) } @@ -141,32 +146,32 @@ public extension ScaleTransformView { } if let options = self.scaleOptions.translation3d { - var x = options.translateRatios.0 * progress - var y = options.translateRatios.1 * abs(progress) - var z = options.translateRatios.2 * abs(progress) - x = max(x, options.minTranslates.0) - x = min(x, options.maxTranslates.0) - y = max(y, options.minTranslates.1) - y = min(y, options.maxTranslates.1) - z = max(z, options.minTranslates.2) - z = min(z, options.maxTranslates.2) + var x = options.translateRatios.0 * progress * scalableView.bounds.width + var y = options.translateRatios.1 * abs(progress) * scalableView.bounds.height + var z = options.translateRatios.2 * abs(progress) * scalableView.bounds.width + x = max(x, options.minTranslateRatios.0 * scalableView.bounds.width) + x = min(x, options.maxTranslateRatios.0 * scalableView.bounds.width) + y = max(y, options.minTranslateRatios.1 * scalableView.bounds.height) + y = min(y, options.maxTranslateRatios.1 * scalableView.bounds.height) + z = max(z, options.minTranslateRatios.2 * scalableView.bounds.width) + z = min(z, options.maxTranslateRatios.2 * scalableView.bounds.width) transform = CATransform3DTranslate(transform, x, y, z) } scalableView.layer.transform = transform } - - @available(iOS 10.0, *) + private func applyBlurEffect(progress: CGFloat) { guard scaleOptions.blurEffectRadiusRatio > 0, scaleOptions.blurEffectEnabled else { + 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/Lib/Scale/ScaleTransformViewOptions+Layout.swift b/Lib/Scale/ScaleTransformViewOptions+Layout.swift new file mode 100644 index 0000000..6625331 --- /dev/null +++ b/Lib/Scale/ScaleTransformViewOptions+Layout.swift @@ -0,0 +1,141 @@ +// +// ScaleTransformViewOptions+Layout.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import UIKit +import Foundation + +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), + maxTranslationRatio: CGPoint(x: 2, y: 0), + blurEffectEnabled: true, + blurEffectRadiusRatio: 0.2 + ) + case .linear: + return Self( + minScale: 0.6, + scaleRatio: 0.4, + translationRatio: CGPoint(x: 0.66, y: 0.2), + maxTranslationRatio: CGPoint(x: 2, y: 0), + keepVerticalSpacingEqual: true, + keepHorizontalSpacingEqual: true, + scaleCurve: .linear, + translationCurve: .linear + ) + case .easeIn: + return Self( + minScale: 0.6, + scaleRatio: 0.4, + translationRatio: CGPoint(x: 0.66, y: 0.2), + keepVerticalSpacingEqual: true, + keepHorizontalSpacingEqual: true, + scaleCurve: .easeIn, + translationCurve: .linear + ) + case .easeOut: + return Self( + minScale: 0.6, + scaleRatio: 0.4, + translationRatio: CGPoint(x: 0.66, y: 0.2), + keepVerticalSpacingEqual: true, + keepHorizontalSpacingEqual: true, + scaleCurve: .linear, + translationCurve: .easeIn + ) + case .rotary: + return Self( + minScale: 0, + scaleRatio: 0.4, + translationRatio: CGPoint(x: 0.1, y: 0.1), + minTranslationRatio: CGPoint(x: -1, y: 0), + maxTranslationRatio: CGPoint(x: 1, y: 1), + rotation3d: ScaleTransformViewOptions.Rotation3dOptions(angle: .pi / 15, minAngle: -.pi / 3, maxAngle: .pi / 3, x: 0, y: 0, z: 1, m34: -0.004), + translation3d: .init(translateRatios: (0.9, 0.1, 0), + minTranslateRatios: (-3, -0.8, -0.3), + maxTranslateRatios: (3, 0.8, -0.3)) + ) + case .cylinder: + return Self( + minScale: 0.55, + maxScale: 0.55, + scaleRatio: 0, + translationRatio: .zero, + minTranslationRatio: .zero, + maxTranslationRatio: .zero, + shadowEnabled: false, + 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 .invertedCylinder: + return Self( + minScale: 1.2, + maxScale: 1.2, + scaleRatio: 0, + translationRatio: .zero, + minTranslationRatio: .zero, + maxTranslationRatio: .zero, + shadowEnabled: false, + rotation3d: .init(angle: .pi / 3, minAngle: -.pi, maxAngle: .pi, x: 0, y: -1, z: 0, m34: -0.002), + translation3d: .init(translateRatios: (0.1, 0, 0), + minTranslateRatios: (-0.05, 0, 0.86), + maxTranslateRatios: (0.05, 0, -0.86)) + ) + 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, + translationRatio: .zero, + minTranslationRatio: .zero, + maxTranslationRatio: .zero, + shadowEnabled: true, + rotation3d: rotation3d, + translation3d: translation3d + ) + } + } +} diff --git a/Lib/Scale/ScaleTransformViewOptions.Rotation3dOptions.swift b/Lib/Scale/ScaleTransformViewOptions.Rotation3dOptions.swift index 6d7a3a1..06b937d 100644 --- a/Lib/Scale/ScaleTransformViewOptions.Rotation3dOptions.swift +++ b/Lib/Scale/ScaleTransformViewOptions.Rotation3dOptions.swift @@ -15,25 +15,25 @@ public extension ScaleTransformViewOptions { // MARK: Properties /// The angle for rotate side views - var angle: CGFloat + public var angle: CGFloat /// The minimum angle for rotation - var minAngle: CGFloat + public var minAngle: CGFloat /// The maximum angle for rotation - var maxAngle: CGFloat + public var maxAngle: CGFloat - var x: CGFloat + public var x: CGFloat - var y: CGFloat + public var y: CGFloat - var z: CGFloat + public var z: CGFloat /// `CATransform3D.m34`, read more: https://stackoverflow.com/questions/3881446/meaning-of-m34-of-catransform3d - var m34: CGFloat + public var m34: CGFloat /// `CALayer.isDoubleSided`, read more: https://developer.apple.com/documentation/quartzcore/calayer/1410924-isdoublesided - var isDoubleSided: Bool = false + public var isDoubleSided: Bool = false // MARK: Lifecycle diff --git a/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift b/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift index 7e56c38..127eb3b 100644 --- a/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift +++ b/Lib/Scale/ScaleTransformViewOptions.Translation3dOptions.swift @@ -10,31 +10,34 @@ import UIKit public extension ScaleTransformViewOptions { - class Translation3dOptions { - + struct Translation3dOptions { + // MARK: Properties - /// The translates(x,y,z) ratios (translateX = progress * translates.x) - var translateRatios: (CGFloat, CGFloat, CGFloat) + /// The translates(x,y,z) ratios + /// (translateX = progress * translates.x * targetView.width) + /// (translateY = progress * translates.y * targetView.height) + /// (translateZ = progress * translates.z * targetView.width) + public var translateRatios: (CGFloat, CGFloat, CGFloat) - /// The minimum translate values - var minTranslates: (CGFloat, CGFloat, CGFloat) + /// The minimum translate ratios + public var minTranslateRatios: (CGFloat, CGFloat, CGFloat) - /// The maximum translate values - var maxTranslates: (CGFloat, CGFloat, CGFloat) + /// The maximum translate ratios + public var maxTranslateRatios: (CGFloat, CGFloat, CGFloat) // MARK: Lifecycle public init( translateRatios: (CGFloat, CGFloat, CGFloat), - minTranslates: (CGFloat, CGFloat, CGFloat), - maxTranslates: (CGFloat, CGFloat, CGFloat) + minTranslateRatios: (CGFloat, CGFloat, CGFloat), + maxTranslateRatios: (CGFloat, CGFloat, CGFloat) ) { self.translateRatios = translateRatios - self.minTranslates = minTranslates - self.maxTranslates = maxTranslates + self.minTranslateRatios = minTranslateRatios + self.maxTranslateRatios = maxTranslateRatios } } - + } diff --git a/Lib/Scale/ScaleTransformViewOptions.swift b/Lib/Scale/ScaleTransformViewOptions.swift index b0a0597..be2d9bf 100644 --- a/Lib/Scale/ScaleTransformViewOptions.swift +++ b/Lib/Scale/ScaleTransformViewOptions.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -public class ScaleTransformViewOptions { +public struct ScaleTransformViewOptions { // MARK: Properties diff --git a/Lib/Snapshot/SnapshotContainerView.swift b/Lib/Snapshot/SnapshotContainerView.swift index ed83301..378461f 100644 --- a/Lib/Snapshot/SnapshotContainerView.swift +++ b/Lib/Snapshot/SnapshotContainerView.swift @@ -15,14 +15,16 @@ public class SnapshotContainerView: UIView { public let snapshots: [UIView] public let identifier: String public let snapshotSize: CGSize + public let pieceSizeRatio: CGSize - private let targetView: UIView + private weak var targetView: UIView? // MARK: Lifecycle public init?(targetView: UIView, pieceSizeRatio: CGSize, identifier: String) { var snapshots: [UIView] = [] + self.pieceSizeRatio = pieceSizeRatio guard pieceSizeRatio.width > 0, pieceSizeRatio.height > 0 else { return nil } diff --git a/Lib/Snapshot/SnapshotTransformView.swift b/Lib/Snapshot/SnapshotTransformView.swift index 862595c..8491414 100644 --- a/Lib/Snapshot/SnapshotTransformView.swift +++ b/Lib/Snapshot/SnapshotTransformView.swift @@ -16,17 +16,21 @@ 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 cache it + /// 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 } + public extension SnapshotTransformView where Self: UICollectionViewCell { /// Default `targetView` for `UICollectionViewCell` is the first subview of @@ -36,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 { @@ -48,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 } } @@ -84,27 +103,53 @@ public extension SnapshotTransformView { } else { snapshot.alpha = 1 // hide the original view, we apply transform on the snapshot - targetView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: -UIScreen.main.bounds.height) + targetView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: -2 * UIScreen.main.bounds.height) } snapshot.transform(progress: progress, options: snapshotOptions) } // 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) { + 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 { + ($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) @@ -133,8 +178,16 @@ private extension SnapshotContainerView { .translatedBy(x: translateX, y: translateY) .scaledBy(x: scale, y: scale) - let rowCount = Int(1.0 / options.pieceSizeRatio.height) - let columnCount = Int(1.0 / options.pieceSizeRatio.width) + var sizeRatioRow = options.pieceSizeRatio.height + if abs(sizeRatioRow) < 0.01 { + sizeRatioRow = 0.01 + } + var sizeRatioColumn = options.pieceSizeRatio.width + if abs(sizeRatioColumn) < 0.01 { + sizeRatioColumn = 0.01 + } + let rowCount = Int(1.0 / sizeRatioRow) + let columnCount = Int(1.0 / sizeRatioColumn) snapshots.enumerated().forEach { index, view in let position = SnapshotTransformViewOptions.PiecePosition( diff --git a/Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift b/Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift new file mode 100644 index 0000000..1c2b8d9 --- /dev/null +++ b/Lib/Snapshot/SnapshotTransformViewOptions+Layout.swift @@ -0,0 +1,108 @@ +// +// SnapshotTransformViewOptions+Layout.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import UIKit +import Foundation + +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), + piecesTranslationRatio: .aggregated([.rowBasedMirror(CGPoint(x: 0, y: -1.8)), .columnBasedMirror(CGPoint(x: -1.8, y: 0))], +), + piecesScaleRatio: .static(.init(width: 0.8, height: 0.8)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 0.7, y: 0) + ) + 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)], +), + piecesTranslationRatio: .aggregated([.rowBasedMirror(CGPoint(x: 1, y: -1)), .columnBasedMirror(CGPoint(x: -1, y: 1))], *), + piecesScaleRatio: .static(.init(width: 0.5, height: 0.5)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 0.7, y: 0) + ) + case .chess: + return Self( + pieceSizeRatio: .init(width: 1.0 / 5.0, height: 1.0 / 10.0), + piecesCornerRadiusRatio: .static(0.5), + piecesAlphaRatio: .columnBasedMirror(0.4), + piecesTranslationRatio: .columnBasedMirror(CGPoint(x: -1, y: 1)), + piecesScaleRatio: .static(.init(width: 0.5, height: 0.5)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 0.7, y: 0) + ) + case .tiles: + return Self( + pieceSizeRatio: .init(width: 1, height: 1.0 / 10.0), + piecesCornerRadiusRatio: .static(0), + piecesAlphaRatio: .static(0.4), + piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.4, y: 0), CGPoint(x: 0.4, y: 0)), + piecesScaleRatio: .static(.init(width: 0, height: 0.1)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 1, y: 0) + ) + case .lines: + return Self( + pieceSizeRatio: .init(width: 1, height: 1.0 / 16.0), + piecesCornerRadiusRatio: .static(0), + piecesAlphaRatio: .static(0.4), + piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.15, y: 0), CGPoint(x: 0.15, y: 0)), + piecesScaleRatio: .static(.init(width: 0.6, height: 0.96)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 0.8, y: 0) + ) + case .bars: + return Self( + pieceSizeRatio: .init(width: 1.0 / 10.0, height: 1), + piecesCornerRadiusRatio: .static(1.2), + piecesAlphaRatio: .static(0.4), + piecesTranslationRatio: .columnOddEven(CGPoint(x: 0, y: -0.1), CGPoint(x: 0, y: 0.1)), + piecesScaleRatio: .static(.init(width: 0.2, height: 0.6)), + containerScaleRatio: 0.1, + containerTranslationRatio: .init(x: 1, y: 0) + ) + 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)], +), + piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.15, y: 0), CGPoint(x: 0.15, y: 0)), + piecesScaleRatio: .columnOddEven(.init(width: 0.1, height: 0.4), .init(width: 0.4, height: 0.1)), + containerScaleRatio: 0.2, + containerTranslationRatio: .init(x: 1, y: 0) + ) + case .fade: + return Self( + pieceSizeRatio: .init(width: 1, height: 1.0 / 10.0), + piecesCornerRadiusRatio: .static(0.1), + piecesAlphaRatio: .rowBased(0.1), + piecesTranslationRatio: .rowBasedMirror(CGPoint(x: 0, y: 0.1)), + piecesScaleRatio: .rowBasedMirror(.init(width: 0.05, height: 0.1)), + containerScaleRatio: 0.7, + containerTranslationRatio: .init(x: 1.9, y: 0) + ) + } + } +} 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 e37c7e6..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,18 +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 { + 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/Lib/Stack/StackTransformViewOptions+Layout.swift b/Lib/Stack/StackTransformViewOptions+Layout.swift new file mode 100644 index 0000000..35a7490 --- /dev/null +++ b/Lib/Stack/StackTransformViewOptions+Layout.swift @@ -0,0 +1,106 @@ +// +// StackTransformViewOptions+Layout.swift +// CollectionViewPagingLayout +// +// Created by Amir on 28/03/2021. +// Copyright © 2021 Amir Khorsandi. All rights reserved. +// + +import UIKit +import Foundation + +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, + alphaFactor: 0.2, + bottomStackAlphaSpeedFactor: 10, + topStackAlphaSpeedFactor: 0.1, + popAngle: .pi / 10, + popOffsetRatio: .init(width: -1.45, height: 0.3) + ) + case .perspective: + return Self( + scaleFactor: 0.1, + minScale: 0.2, + maxStackSize: 6, + spacingFactor: 0.08, + alphaFactor: 0.0, + perspectiveRatio: 0.3, + shadowRadius: 5, + popAngle: .pi / 10, + popOffsetRatio: .init(width: -1.45, height: 0.3), + stackPosition: CGPoint(x: 1, y: 0) + ) + case .rotary: + return Self( + scaleFactor: -0.03, + minScale: 0.2, + maxStackSize: 3, + spacingFactor: 0.01, + alphaFactor: 0.1, + shadowRadius: 8, + stackRotateAngel: .pi / 16, + popAngle: .pi / 4, + popOffsetRatio: .init(width: -1.45, height: 0.4), + stackPosition: CGPoint(x: 0, y: 1) + ) + case .vortex: + return Self( + scaleFactor: -0.15, + minScale: 0.2, + maxScale: nil, + maxStackSize: 4, + spacingFactor: 0, + alphaFactor: 0.4, + topStackAlphaSpeedFactor: 1, + perspectiveRatio: -0.3, + shadowEnabled: false, + popAngle: .pi, + popOffsetRatio: .zero, + stackPosition: CGPoint(x: 0, y: 1) + ) + case .reverse: + return Self( + scaleFactor: 0.1, + maxScale: nil, + maxStackSize: 4, + spacingFactor: 0.08, + shadowRadius: 8, + popAngle: -.pi / 4, + popOffsetRatio: .init(width: 1.45, height: 0.4), + stackPosition: CGPoint(x: -1, y: -0.2), + reverse: true + ) + case .blur: + return Self( + scaleFactor: 0.1, + maxScale: nil, + maxStackSize: 7, + spacingFactor: 0.06, + topStackAlphaSpeedFactor: 0.1, + perspectiveRatio: 0.04, + shadowRadius: 8, + popAngle: -.pi / 4, + popOffsetRatio: .init(width: 1.45, height: 0.4), + stackPosition: CGPoint(x: -1, y: 0), + reverse: true, + blurEffectEnabled: true, + maxBlurEffectRadius: 0.08 + ) + } + } +} diff --git a/Lib/Stack/StackTransformViewOptions.swift b/Lib/Stack/StackTransformViewOptions.swift index 6f3f561..fd91606 100644 --- a/Lib/Stack/StackTransformViewOptions.swift +++ b/Lib/Stack/StackTransformViewOptions.swift @@ -108,7 +108,6 @@ public struct StackTransformViewOptions { spacingFactor: CGFloat = 0.03, maxSpacing: CGFloat? = nil, alphaFactor: CGFloat = 0.0, - minAlpha: CGFloat = 0.0, bottomStackAlphaSpeedFactor: CGFloat = 0.9, topStackAlphaSpeedFactor: CGFloat = 0.3, perspectiveRatio: CGFloat = 0, 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 caf766e..f1217d1 100644 --- a/Lib/TransformableView.swift +++ b/Lib/TransformableView.swift @@ -11,8 +11,16 @@ import UIKit public protocol TransformableView { + /// 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) @@ -33,3 +41,27 @@ public extension TransformableView { Int(-abs(round(progress))) } } + + +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 + } +} + + +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/Utilities/UIView+Utilities.swift b/Lib/Utilities/UIView+Utilities.swift index 9a560fb..ed8db9f 100644 --- a/Lib/Utilities/UIView+Utilities.swift +++ b/Lib/Utilities/UIView+Utilities.swift @@ -10,15 +10,18 @@ import UIKit extension UIView { - public func fill(with view: UIView, edges: UIEdgeInsets = .zero) { + @discardableResult + public func fill(with view: UIView, edges: UIEdgeInsets = .zero) -> [NSLayoutConstraint] { view.translatesAutoresizingMaskIntoConstraints = false addSubview(view) - addConstraints([ + let constraints = [ NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: edges.left), - NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: edges.right), NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: edges.top), + NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: edges.right), NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: edges.bottom) - ]) + ] + addConstraints(constraints) + return constraints } public func center(to view: UIView) { 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 3b27275..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,57 +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) -## Preview +## Previews + +### Layout Designer +[](https://apps.apple.com/nl/app/layout-designer/id1507238011?l=en&mt=12) + + + +#### Custom implementations, UIKit: `TransformableView`, SwiftUI: `TransformPageView` +Click on image to see the code

- - - -

-### SnapshotTransformView +[](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` +

- - - - - - - - + +[](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)

-### ScaleTransformView + +#### 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)

-### StackTransformView +### 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 -This is a custom `UICollectionViewLayout` that gives you the ability to apply transforms easily on the cells -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. -See [How to use](https://github.com/amirdew/CollectionViewPagingLayout#how-to-use) part 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) @@ -97,138 +126,34 @@ File > Swift Packages > Add Package Dependency Just add all the files under `Lib` directory to your project ## How to use -- 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. -*Note:* you can use [Prepared Transformable Protocols](#prepared-transformable-protocols) instead of `TransformableView` if you don't want a custom effect! +### 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 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`: +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!. -```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 +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) -## 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 - -You can control the current page by following public 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 the changes by setting `CollectionViewPagingLayout.delegate`: - -```swift -public protocol CollectionViewPagingLayoutDelegate: class { - func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) -} -``` +## Limitations +- **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. +- **It doesn't support RTL layouts:** +however, you can achieve a similar result by tweaking options, for instance try `StackTransformViewOptions.Layout.reverse` -## Limitations -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. +## Credit +- [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/AppKitGlue-Bridging-Header.h b/Samples/AppKitGlue/AppKitGlue-Bridging-Header.h new file mode 100644 index 0000000..fe85d53 --- /dev/null +++ b/Samples/AppKitGlue/AppKitGlue-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "AppKitBridge.h" diff --git a/Samples/AppKitGlue/Info.plist b/Samples/AppKitGlue/Info.plist new file mode 100644 index 0000000..ca5c428 --- /dev/null +++ b/Samples/AppKitGlue/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Amir Khorsandi. All rights reserved. + NSPrincipalClass + AppKitGlue.MacApp + + diff --git a/Samples/AppKitGlue/MacApp.swift b/Samples/AppKitGlue/MacApp.swift new file mode 100644 index 0000000..647c6d8 --- /dev/null +++ b/Samples/AppKitGlue/MacApp.swift @@ -0,0 +1,59 @@ +// +// MacApp.swift +// AppKitGlue +// +// Created by Amir on 05/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Cocoa +import Combine + +class MacApp: NSObject, AppKitBridge { + + private var window: NSWindow! + private var cancellable: Cancellable! + + func initialise() { + 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() { + 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) + } + } + + private func hideToolbar() { + 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 0395cc3..829be8e 100644 --- a/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj +++ b/Samples/PagingLayoutSamples.xcodeproj/project.pbxproj @@ -3,13 +3,41 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ - 291224692400352F001B603A /* StackShapeCollectionViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291224682400352F001B603A /* StackShapeCollectionViewCells.swift */; }; + 292489B02461A97900A316B0 /* LayoutDesignerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292489AF2461A97900A316B0 /* LayoutDesignerViewController.swift */; }; + 292489B22461E31300A316B0 /* LayoutDesignerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 292489B12461E31300A316B0 /* LayoutDesignerViewController.xib */; }; + 292489BD2461F08B00A316B0 /* AppKitGlue.bundle in Embed PlugIns */ = {isa = PBXBuildFile; fileRef = 292489B72461F07D00A316B0 /* AppKitGlue.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 292489C32461FD6A00A316B0 /* MacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292489C22461FD6A00A316B0 /* MacApp.swift */; }; + 292489C52461FE2700A316B0 /* Catalyst.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292489C42461FE2700A316B0 /* Catalyst.swift */; }; 2925CDDF23D4D21F00243F5F /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2925CDDE23D4D21F00243F5F /* Card.swift */; }; - 29B815B12414132100F1C824 /* SnapshotShapeCollectionViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B815B02414132100F1C824 /* SnapshotShapeCollectionViewCells.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 */; }; + 29A2D3D124B738E5005A0F6B /* LayoutDesignerIntroViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A2D3D024B738E5005A0F6B /* LayoutDesignerIntroViewModel.swift */; }; + 29A2D3D324B73A19005A0F6B /* LayoutDesignerIntroCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A2D3D224B73A19005A0F6B /* LayoutDesignerIntroCell.swift */; }; + 29A2D3D524B73AB7005A0F6B /* LayoutDesignerIntroInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A2D3D424B73AB7005A0F6B /* LayoutDesignerIntroInfo.swift */; }; + 29A2D3D724B73CB1005A0F6B /* LayoutDesignerIntroCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29A2D3D624B73CB1005A0F6B /* LayoutDesignerIntroCell.xib */; }; + 29B5A72024A7B02900C9843E /* ShapeLayout+StackOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A71F24A7B02900C9843E /* ShapeLayout+StackOptions.swift */; }; + 29B5A72224A7B06300C9843E /* ShapeLayout+SnapshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72124A7B06300C9843E /* ShapeLayout+SnapshotOptions.swift */; }; + 29B5A72424A8CC4B00C9843E /* CGFloat+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72324A8CC4B00C9843E /* CGFloat+String.swift */; }; + 29B5A72624A8D8B300C9843E /* OptionsCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72524A8D8B300C9843E /* OptionsCodeGenerator.swift */; }; + 29B5A72824A8D8F700C9843E /* TransformCurve+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72724A8D8F700C9843E /* TransformCurve+Name.swift */; }; + 29B5A72A24A8D94300C9843E /* Values+Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72924A8D94300C9843E /* Values+Pair.swift */; }; + 29B5A72C24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5A72B24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift */; }; + 29BEC4D52476DD9D004BA505 /* LayoutDesignerOptionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BEC4D42476DD9D004BA505 /* LayoutDesignerOptionsTableView.swift */; }; + 29CE3D4624B7763C00380DCD /* SampleProject.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 29CE3D4524B7763C00380DCD /* SampleProject.bundle */; }; 29D9F94323F7F98800656A67 /* ShapesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F94223F7F98800656A67 /* ShapesViewController.swift */; }; 29D9F94523F7F99400656A67 /* ShapesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F94423F7F99400656A67 /* ShapesViewModel.swift */; }; 29D9F94723F7F9B700656A67 /* ShapesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29D9F94623F7F9B700656A67 /* ShapesViewController.xib */; }; @@ -19,7 +47,14 @@ 29D9F95023F806C400656A67 /* Optional+Let.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F94F23F806C400656A67 /* Optional+Let.swift */; }; 29D9F95323F8685C00656A67 /* BaseShapeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95223F8685C00656A67 /* BaseShapeCollectionViewCell.swift */; }; 29D9F95923F874E900656A67 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95823F874E900656A67 /* GradientView.swift */; }; - 29D9F95B23F88A6900656A67 /* ScaleShapeCollectionViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D9F95A23F88A6900656A67 /* ScaleShapeCollectionViewCells.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 */; }; AB1BBA9B23CA5179004E5C3B /* CardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1BBA9823CA5178004E5C3B /* CardCellViewModel.swift */; }; AB1BBA9C23CA5179004E5C3B /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1BBA9923CA5178004E5C3B /* CardCollectionViewCell.swift */; }; @@ -61,10 +96,67 @@ ABC242D023B6861000DBD4D6 /* PhotoCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = ABC242CF23B6861000DBD4D6 /* PhotoCollectionViewCell.xib */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 292489BE2461F08B00A316B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AB500A2723B104E20056BE37 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 292489B62461F07D00A316B0; + remoteInfo = AppKitGlue; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 292489C02461F08B00A316B0 /* Embed PlugIns */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 292489BD2461F08B00A316B0 /* AppKitGlue.bundle in Embed PlugIns */, + ); + name = "Embed PlugIns"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ - 291224682400352F001B603A /* StackShapeCollectionViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackShapeCollectionViewCells.swift; sourceTree = ""; }; + 292489AD2461A57900A316B0 /* Paging Layout.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Paging Layout.entitlements"; sourceTree = ""; }; + 292489AF2461A97900A316B0 /* LayoutDesignerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerViewController.swift; sourceTree = ""; }; + 292489B12461E31300A316B0 /* LayoutDesignerViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayoutDesignerViewController.xib; sourceTree = ""; }; + 292489B72461F07D00A316B0 /* AppKitGlue.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppKitGlue.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 292489B92461F07D00A316B0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 292489C12461FD6900A316B0 /* AppKitGlue-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppKitGlue-Bridging-Header.h"; sourceTree = ""; }; + 292489C22461FD6A00A316B0 /* MacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacApp.swift; sourceTree = ""; }; + 292489C42461FE2700A316B0 /* Catalyst.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Catalyst.swift; sourceTree = ""; }; + 292489CA24620BD600A316B0 /* AppKitBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppKitBridge.h; sourceTree = ""; }; + 292489CB24620C4D00A316B0 /* PagingLayoutSamples-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PagingLayoutSamples-Bridging-Header.h"; sourceTree = ""; }; 2925CDDE23D4D21F00243F5F /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; - 29B815B02414132100F1C824 /* SnapshotShapeCollectionViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotShapeCollectionViewCells.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 = ""; }; + 29A2D3D024B738E5005A0F6B /* LayoutDesignerIntroViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerIntroViewModel.swift; sourceTree = ""; }; + 29A2D3D224B73A19005A0F6B /* LayoutDesignerIntroCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerIntroCell.swift; sourceTree = ""; }; + 29A2D3D424B73AB7005A0F6B /* LayoutDesignerIntroInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerIntroInfo.swift; sourceTree = ""; }; + 29A2D3D624B73CB1005A0F6B /* LayoutDesignerIntroCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayoutDesignerIntroCell.xib; sourceTree = ""; }; + 29B5A71F24A7B02900C9843E /* ShapeLayout+StackOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShapeLayout+StackOptions.swift"; sourceTree = ""; }; + 29B5A72124A7B06300C9843E /* ShapeLayout+SnapshotOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShapeLayout+SnapshotOptions.swift"; sourceTree = ""; }; + 29B5A72324A8CC4B00C9843E /* CGFloat+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+String.swift"; sourceTree = ""; }; + 29B5A72524A8D8B300C9843E /* OptionsCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsCodeGenerator.swift; sourceTree = ""; }; + 29B5A72724A8D8F700C9843E /* TransformCurve+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransformCurve+Name.swift"; sourceTree = ""; }; + 29B5A72924A8D94300C9843E /* Values+Pair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Values+Pair.swift"; sourceTree = ""; }; + 29B5A72B24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBlurEffect.Style+Name.swift"; sourceTree = ""; }; + 29BEC4D42476DD9D004BA505 /* LayoutDesignerOptionsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDesignerOptionsTableView.swift; sourceTree = ""; }; + 29CE3D4524B7763C00380DCD /* SampleProject.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = SampleProject.bundle; sourceTree = ""; }; 29D9F94223F7F98800656A67 /* ShapesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesViewController.swift; sourceTree = ""; }; 29D9F94423F7F99400656A67 /* ShapesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesViewModel.swift; sourceTree = ""; }; 29D9F94623F7F9B700656A67 /* ShapesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShapesViewController.xib; sourceTree = ""; }; @@ -74,7 +166,14 @@ 29D9F94F23F806C400656A67 /* Optional+Let.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Let.swift"; sourceTree = ""; }; 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 /* ScaleShapeCollectionViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleShapeCollectionViewCells.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 = ""; }; 89FF77F458B1EB2029D8978E /* Pods-PagingLayoutSamples.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PagingLayoutSamples.debug.xcconfig"; path = "Target Support Files/Pods-PagingLayoutSamples/Pods-PagingLayoutSamples.debug.xcconfig"; sourceTree = ""; }; A489BA28D16931A0BE34BD0B /* Pods_PagingLayoutSamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PagingLayoutSamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -85,7 +184,7 @@ AB1BBA9923CA5178004E5C3B /* CardCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCollectionViewCell.swift; sourceTree = ""; }; AB1BBA9A23CA5179004E5C3B /* CardCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CardCollectionViewCell.xib; sourceTree = ""; }; AB1E03AE23B25CE70087F904 /* PageControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControlView.swift; sourceTree = ""; }; - AB500A2F23B104E20056BE37 /* Paging Layout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Paging Layout.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB500A2F23B104E20056BE37 /* Layout Designer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Layout Designer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AB500A3223B104E20056BE37 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AB500A3B23B104E60056BE37 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AB500A3E23B104E60056BE37 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -121,6 +220,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 292489B42461F07D00A316B0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; AB500A2C23B104E20056BE37 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -140,6 +246,79 @@ name = Frameworks; sourceTree = ""; }; + 292489AE2461A96600A316B0 /* LayoutDesigner */ = { + isa = PBXGroup; + children = ( + 29A2D3CB24B73870005A0F6B /* Intro */, + 29FF296024A631DC00C83DF9 /* Code */, + 2977657A247452BC00835DBD /* Options */, + 292489AF2461A97900A316B0 /* LayoutDesignerViewController.swift */, + 294BA3C224A77A73008D0569 /* LayoutDesignerViewModel.swift */, + 292489B12461E31300A316B0 /* LayoutDesignerViewController.xib */, + 29B5A72524A8D8B300C9843E /* OptionsCodeGenerator.swift */, + ); + path = LayoutDesigner; + sourceTree = ""; + }; + 292489B82461F07D00A316B0 /* AppKitGlue */ = { + isa = PBXGroup; + children = ( + 292489B92461F07D00A316B0 /* Info.plist */, + 292489C22461FD6A00A316B0 /* MacApp.swift */, + 292489C12461FD6900A316B0 /* AppKitGlue-Bridging-Header.h */, + ); + path = AppKitGlue; + sourceTree = ""; + }; + 29519ACA262B5DD400D8A4A3 /* Shapes */ = { + isa = PBXGroup; + children = ( + 29519ACB262B5DE400D8A4A3 /* ShapesListView.swift */, + 29519ACD262B646E00D8A4A3 /* ShapesListView.ShapeView.swift */, + ); + path = Shapes; + sourceTree = ""; + }; + 2977657A247452BC00835DBD /* Options */ = { + isa = PBXGroup; + children = ( + 29BEC4D62476DDA7004BA505 /* cell */, + 29BEC4D42476DD9D004BA505 /* LayoutDesignerOptionsTableView.swift */, + ); + path = Options; + sourceTree = ""; + }; + 29834F2925C5976300896343 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 29519ACA262B5DD400D8A4A3 /* Shapes */, + 29DD1E322627707A00846F7B /* WeatherTabView */, + 29834F2A25C5977300896343 /* DevicesView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 29A2D3CB24B73870005A0F6B /* Intro */ = { + isa = PBXGroup; + children = ( + 29A2D3CC24B738CC005A0F6B /* LayoutDesignerIntroViewController.swift */, + 29A2D3D024B738E5005A0F6B /* LayoutDesignerIntroViewModel.swift */, + 29A2D3D224B73A19005A0F6B /* LayoutDesignerIntroCell.swift */, + 29A2D3D624B73CB1005A0F6B /* LayoutDesignerIntroCell.xib */, + ); + path = Intro; + sourceTree = ""; + }; + 29BEC4D62476DDA7004BA505 /* cell */ = { + isa = PBXGroup; + children = ( + 2977657F247454BC00835DBD /* LayoutDesignerOptionCellViewModel.swift */, + 2977657B2474531200835DBD /* LayoutDesignerOptionCell.swift */, + 2977657D2474531D00835DBD /* LayoutDesignerOptionCell.xib */, + ); + path = cell; + sourceTree = ""; + }; 29D9F94123F7F92000656A67 /* Shapes */ = { isa = PBXGroup; children = ( @@ -149,6 +328,9 @@ 29D9F94223F7F98800656A67 /* ShapesViewController.swift */, 29D9F94423F7F99400656A67 /* ShapesViewModel.swift */, 29D9F94623F7F9B700656A67 /* ShapesViewController.xib */, + 2993722224A79A9C0026D52F /* ShapeLayout+ScaleOptions.swift */, + 29B5A71F24A7B02900C9843E /* ShapeLayout+StackOptions.swift */, + 29B5A72124A7B06300C9843E /* ShapeLayout+SnapshotOptions.swift */, ); path = Shapes; sourceTree = ""; @@ -167,13 +349,44 @@ isa = PBXGroup; children = ( 29D9F95223F8685C00656A67 /* BaseShapeCollectionViewCell.swift */, - 29D9F95A23F88A6900656A67 /* ScaleShapeCollectionViewCells.swift */, - 291224682400352F001B603A /* StackShapeCollectionViewCells.swift */, - 29B815B02414132100F1C824 /* SnapshotShapeCollectionViewCells.swift */, + 29D9F95A23F88A6900656A67 /* ShapeCollectionViewCells.swift */, ); 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 = ( + 29FF296124A6321100C83DF9 /* LayoutDesignerCodePreviewViewController.swift */, + 29A2D3A924B72895005A0F6B /* LayoutDesignerCodePreviewViewController.xib */, + 29FF296324A6321B00C83DF9 /* LayoutDesignerCodePreviewViewModel.swift */, + ); + path = Code; + sourceTree = ""; + }; 4D87DB0F34A1E0FEF9A22214 /* Pods */ = { isa = PBXGroup; children = ( @@ -219,6 +432,7 @@ isa = PBXGroup; children = ( AB500A3123B104E20056BE37 /* PagingLayoutSamples */, + 292489B82461F07D00A316B0 /* AppKitGlue */, AB500A3023B104E20056BE37 /* Products */, 4D87DB0F34A1E0FEF9A22214 /* Pods */, 05F53E609F3F8B841D87E931 /* Frameworks */, @@ -228,7 +442,8 @@ AB500A3023B104E20056BE37 /* Products */ = { isa = PBXGroup; children = ( - AB500A2F23B104E20056BE37 /* Paging Layout.app */, + AB500A2F23B104E20056BE37 /* Layout Designer.app */, + 292489B72461F07D00A316B0 /* AppKitGlue.bundle */, ); name = Products; sourceTree = ""; @@ -236,14 +451,19 @@ AB500A3123B104E20056BE37 /* PagingLayoutSamples */ = { isa = PBXGroup; children = ( + 292489AD2461A57900A316B0 /* Paging Layout.entitlements */, AB1E03AD23B25CD30087F904 /* CustomViews */, AB500A5223B152170056BE37 /* Modules */, AB500A5123B152130056BE37 /* Models */, AB500A5323B152400056BE37 /* Utilities */, AB500A3223B104E20056BE37 /* AppDelegate.swift */, + 292489C42461FE2700A316B0 /* Catalyst.swift */, AB500A3B23B104E60056BE37 /* Assets.xcassets */, AB500A3D23B104E60056BE37 /* LaunchScreen.storyboard */, AB500A4023B104E60056BE37 /* Info.plist */, + 292489CA24620BD600A316B0 /* AppKitBridge.h */, + 292489CB24620C4D00A316B0 /* PagingLayoutSamples-Bridging-Header.h */, + 29CE3D4524B7763C00380DCD /* SampleProject.bundle */, ); path = PagingLayoutSamples; sourceTree = ""; @@ -267,6 +487,7 @@ 2925CDDE23D4D21F00243F5F /* Card.swift */, ABA0DA0023F93CA3004A9C18 /* ShapeLayout.swift */, ABA0DA0223F93CDB004A9C18 /* Shape.swift */, + 29A2D3D424B73AB7005A0F6B /* LayoutDesignerIntroInfo.swift */, ); path = Models; sourceTree = ""; @@ -274,11 +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 = ""; @@ -288,8 +508,14 @@ children = ( AB500A4D23B13C5C0056BE37 /* NibBased.swift */, ABA0DA0D23F98ECD004A9C18 /* UICollectionViewCell+Utilities.swift */, + 2949CB262476EA8C000CC073 /* UITableView+Utilities.swift */, AB500A5423B152500056BE37 /* ViewModelBased.swift */, 29D9F94F23F806C400656A67 /* Optional+Let.swift */, + 29B5A72324A8CC4B00C9843E /* CGFloat+String.swift */, + 29B5A72724A8D8F700C9843E /* TransformCurve+Name.swift */, + 29B5A72B24A8DD7100C9843E /* UIBlurEffect.Style+Name.swift */, + 29B5A72924A8D94300C9843E /* Values+Pair.swift */, + 29DD1E2A26275E1D00846F7B /* VisualEffectView.swift */, ); path = Utilities; sourceTree = ""; @@ -365,6 +591,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 292489B62461F07D00A316B0 /* AppKitGlue */ = { + isa = PBXNativeTarget; + buildConfigurationList = 292489BA2461F07D00A316B0 /* Build configuration list for PBXNativeTarget "AppKitGlue" */; + buildPhases = ( + 292489B32461F07D00A316B0 /* Sources */, + 292489B42461F07D00A316B0 /* Frameworks */, + 292489B52461F07D00A316B0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AppKitGlue; + productName = AppKitGlue; + productReference = 292489B72461F07D00A316B0 /* AppKitGlue.bundle */; + productType = "com.apple.product-type.bundle"; + }; AB500A2E23B104E20056BE37 /* PagingLayoutSamples */ = { isa = PBXNativeTarget; buildConfigurationList = AB500A4323B104E60056BE37 /* Build configuration list for PBXNativeTarget "PagingLayoutSamples" */; @@ -375,14 +618,16 @@ AB500A2C23B104E20056BE37 /* Frameworks */, AB500A2D23B104E20056BE37 /* Resources */, 16CABBE36A64D6ED31E82309 /* [CP] Embed Pods Frameworks */, + 292489C02461F08B00A316B0 /* Embed PlugIns */, ); buildRules = ( ); dependencies = ( + 292489BF2461F08B00A316B0 /* PBXTargetDependency */, ); name = PagingLayoutSamples; productName = CollectionViewPagingLayout; - productReference = AB500A2F23B104E20056BE37 /* Paging Layout.app */; + productReference = AB500A2F23B104E20056BE37 /* Layout Designer.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -392,9 +637,13 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1120; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = "Amir Khorsandi"; TargetAttributes = { + 292489B62461F07D00A316B0 = { + CreatedOnToolsVersion = 11.4; + LastSwiftMigration = 1140; + }; AB500A2E23B104E20056BE37 = { CreatedOnToolsVersion = 11.2.1; }; @@ -414,11 +663,19 @@ projectRoot = ""; targets = ( AB500A2E23B104E20056BE37 /* PagingLayoutSamples */, + 292489B62461F07D00A316B0 /* AppKitGlue */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 292489B52461F07D00A316B0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; AB500A2D23B104E20056BE37 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -428,14 +685,19 @@ ABC242C523B6822A00DBD4D6 /* GalleryViewController.xib in Resources */, AB500A5F23B154950056BE37 /* FruitsCollectionViewCell.xib in Resources */, AB1BBA9F23CA7BE3004E5C3B /* CardsViewController.xib in Resources */, + 2977657E2474531D00835DBD /* LayoutDesignerOptionCell.xib in Resources */, + 29CE3D4624B7763C00380DCD /* SampleProject.bundle in Resources */, 29D9F94723F7F9B700656A67 /* ShapesViewController.xib in Resources */, ABC242D023B6861000DBD4D6 /* PhotoCollectionViewCell.xib in Resources */, ABA0DA0A23F98B70004A9C18 /* ShapeCardView.xib in Resources */, + 292489B22461E31300A316B0 /* LayoutDesignerViewController.xib in Resources */, AB500A4C23B13BC90056BE37 /* FruitsViewController.xib in Resources */, AB500A3C23B104E60056BE37 /* Assets.xcassets in Resources */, AB1BBA9D23CA5179004E5C3B /* CardCollectionViewCell.xib in Resources */, + 29A2D3D724B73CB1005A0F6B /* LayoutDesignerIntroCell.xib in Resources */, AB7C1E0823B4E2C0006441DE /* MainViewController.xib in Resources */, ABA1A72E23B42247006A46A3 /* PriceTagView.xib in Resources */, + 29A2D3AA24B72895005A0F6B /* LayoutDesignerCodePreviewViewController.xib in Resources */, ABA1A73323B422B9006A46A3 /* QuantityControllerView.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -503,52 +765,97 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 292489B32461F07D00A316B0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 292489C32461FD6A00A316B0 /* MacApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; AB500A2B23B104E20056BE37 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ABC242CE23B6860700DBD4D6 /* PhotoCollectionViewCell.swift in Sources */, + 29A2D3D324B73A19005A0F6B /* LayoutDesignerIntroCell.swift in Sources */, 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 */, AB1E03AF23B25CE70087F904 /* PageControlView.swift in Sources */, + 29B5A72224A7B06300C9843E /* ShapeLayout+SnapshotOptions.swift in Sources */, + 29A2D3D524B73AB7005A0F6B /* LayoutDesignerIntroInfo.swift in Sources */, + 29776580247454BC00835DBD /* LayoutDesignerOptionCellViewModel.swift in Sources */, 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 */, ABC242C323B6822200DBD4D6 /* GalleryViewController.swift in Sources */, + 2993722324A79A9C0026D52F /* ShapeLayout+ScaleOptions.swift in Sources */, AB1BBAA023CA7BE6004E5C3B /* CardsViewModel.swift in Sources */, + 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 */, AB7C1E0623B4E2B2006441DE /* MainViewController.swift in Sources */, AB500A5D23B1547C0056BE37 /* FruitCellViewModel.swift in Sources */, 29D9F94E23F7FDA600656A67 /* LayoutTypeCellViewModel.swift in Sources */, - 29D9F95B23F88A6900656A67 /* ScaleShapeCollectionViewCells.swift in Sources */, + 29D9F95B23F88A6900656A67 /* ShapeCollectionViewCells.swift in Sources */, ABA0DA0123F93CA3004A9C18 /* ShapeLayout.swift in Sources */, + 294BA3C324A77A73008D0569 /* LayoutDesignerViewModel.swift in Sources */, 29D9F94C23F7FC5800656A67 /* LayoutTypeCollectionViewCell.swift in Sources */, AB1BBA9C23CA5179004E5C3B /* CardCollectionViewCell.swift in Sources */, 29D9F95323F8685C00656A67 /* BaseShapeCollectionViewCell.swift in Sources */, - 291224692400352F001B603A /* StackShapeCollectionViewCells.swift in Sources */, 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 */, - 29B815B12414132100F1C824 /* SnapshotShapeCollectionViewCells.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 */, AB500A3323B104E20056BE37 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 292489BF2461F08B00A316B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = maccatalyst; + target = 292489B62461F07D00A316B0 /* AppKitGlue */; + targetProxy = 292489BE2461F08B00A316B0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ AB500A3D23B104E60056BE37 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -561,9 +868,61 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 292489BB2461F07D00A316B0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 4J5W7CJ2ZV; + INFOPLIST_FILE = AppKitGlue/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = amir.app.AppKitGlue; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "AppKitGlue/AppKitGlue-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 292489BC2461F07D00A316B0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 4J5W7CJ2ZV; + INFOPLIST_FILE = AppKitGlue/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = amir.app.AppKitGlue; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "AppKitGlue/AppKitGlue-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; AB500A4123B104E60056BE37 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -587,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; @@ -594,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; @@ -611,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; @@ -624,6 +986,7 @@ AB500A4223B104E60056BE37 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -647,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; @@ -654,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; @@ -665,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; @@ -679,18 +1045,25 @@ isa = XCBuildConfiguration; baseConfigurationReference = 89FF77F458B1EB2029D8978E /* Pods-PagingLayoutSamples.debug.xcconfig */; 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; DEVELOPMENT_TEAM = 4J5W7CJ2ZV; INFOPLIST_FILE = PagingLayoutSamples/Info.plist; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.amir.paginglayout; - PRODUCT_NAME = "Paging Layout"; + 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; + TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Debug; }; @@ -698,24 +1071,40 @@ isa = XCBuildConfiguration; baseConfigurationReference = 444EC9E8B3BA262F3697984F /* Pods-PagingLayoutSamples.release.xcconfig */; 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; DEVELOPMENT_TEAM = 4J5W7CJ2ZV; INFOPLIST_FILE = PagingLayoutSamples/Info.plist; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.amir.paginglayout; - PRODUCT_NAME = "Paging Layout"; + 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; + TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 292489BA2461F07D00A316B0 /* Build configuration list for PBXNativeTarget "AppKitGlue" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 292489BB2461F07D00A316B0 /* Debug */, + 292489BC2461F07D00A316B0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; AB500A2A23B104E20056BE37 /* Build configuration list for PBXProject "PagingLayoutSamples" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme b/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme index b087b53..c405f46 100644 --- a/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme +++ b/Samples/PagingLayoutSamples.xcodeproj/xcshareddata/xcschemes/PagingLayoutSamples.xcscheme @@ -1,6 +1,6 @@ @@ -45,7 +45,7 @@ @@ -62,7 +62,7 @@ diff --git a/Samples/PagingLayoutSamples/AppDelegate.swift b/Samples/PagingLayoutSamples/AppDelegate.swift index 0c8b6a5..a322309 100644 --- a/Samples/PagingLayoutSamples/AppDelegate.swift +++ b/Samples/PagingLayoutSamples/AppDelegate.swift @@ -7,24 +7,40 @@ // import UIKit +import SwiftUI @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + var window: UIWindow? var navigationController: UINavigationController! - + + override init() { + super.init() + Catalyst.bridge?.initialise() + } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - - window = UIWindow(frame: UIScreen.main.bounds) + window = UIWindow() + window?.backgroundColor = .clear navigationController = UINavigationController() + navigationController.view.backgroundColor = .clear navigationController.isNavigationBarHidden = true - navigationController.setViewControllers([MainViewController.instantiate()], animated: false) + 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/AppKitBridge.h b/Samples/PagingLayoutSamples/AppKitBridge.h new file mode 100644 index 0000000..35ee611 --- /dev/null +++ b/Samples/PagingLayoutSamples/AppKitBridge.h @@ -0,0 +1,15 @@ +// +// AppKitBridge.h +// PagingLayoutSamples +// +// Created by Amir on 05/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +#import + +@protocol AppKitBridge + +- (void)initialise; + +@end 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/Colors/DesignerBackground.colorset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Colors/DesignerBackground.colorset/Contents.json new file mode 100644 index 0000000..e8e8d50 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Colors/DesignerBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD7", + "green" : "0x45", + "red" : "0x29" + } + }, + "idiom" : "universal" + } + ], + "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/Intro/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/Contents.json new file mode 100644 index 0000000..bc5d03e --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intro01.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "intro01@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "intro01@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01.png new file mode 100644 index 0000000..54a6978 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@2x.png new file mode 100644 index 0000000..03a8452 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@3x.png new file mode 100644 index 0000000..28b4519 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro01.imageset/intro01@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/Contents.json new file mode 100644 index 0000000..883bf57 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intro02.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "intro02@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "intro02@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02.png new file mode 100644 index 0000000..10318a4 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@2x.png new file mode 100644 index 0000000..6850a3a Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@3x.png new file mode 100644 index 0000000..50fead2 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro02.imageset/intro02@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/Contents.json new file mode 100644 index 0000000..933341f --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intro03.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "intro03@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "intro03@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03.png new file mode 100644 index 0000000..5d85eeb Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@2x.png new file mode 100644 index 0000000..31d9f98 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@3x.png new file mode 100644 index 0000000..3017561 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro03.imageset/intro03@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/Contents.json new file mode 100644 index 0000000..0de9a80 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intro04.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "intro04@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "intro04@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04.png new file mode 100644 index 0000000..b7b7bdf Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@2x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@2x.png new file mode 100644 index 0000000..3b7ccea Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@2x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@3x.png b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@3x.png new file mode 100644 index 0000000..a960ae5 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/intro04.imageset/intro04@3x.png differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/Contents.json new file mode 100644 index 0000000..3413cc0 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logoForIntro.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/logoForIntro.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/logoForIntro.pdf new file mode 100644 index 0000000..aed3511 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Intro/logoForIntro.imageset/logoForIntro.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/Contents.json new file mode 100644 index 0000000..f006886 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "scaleWhite.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/scaleWhite.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/scaleWhite.pdf new file mode 100644 index 0000000..ab7f372 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Main/scaleWhite.imageset/scaleWhite.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/Contents.json new file mode 100644 index 0000000..ffb59cc --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "snapshotWhite.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/snapshotWhite.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/snapshotWhite.pdf new file mode 100644 index 0000000..0f9c503 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Main/snapshotWhite.imageset/snapshotWhite.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/Contents.json new file mode 100644 index 0000000..b1d12e0 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stackWhite.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/stackWhite.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/stackWhite.pdf new file mode 100644 index 0000000..2a8910d Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Main/stackWhite.imageset/stackWhite.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/Contents.json new file mode 100644 index 0000000..fcee656 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "copyButton.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/copyButton.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/copyButton.pdf new file mode 100644 index 0000000..810d75d Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/copyButton.imageset/copyButton.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/Contents.json new file mode 100644 index 0000000..8d759f6 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "downloadButton.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/downloadButton.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/downloadButton.pdf new file mode 100644 index 0000000..e08289b Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/downloadButton.imageset/downloadButton.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/Contents.json new file mode 100644 index 0000000..2f4fa61 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "grayArrowLeft.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/grayArrowLeft.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/grayArrowLeft.pdf new file mode 100644 index 0000000..afe7671 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowLeft.imageset/grayArrowLeft.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/Contents.json new file mode 100644 index 0000000..383333d --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "grayArrowRight.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/grayArrowRight.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/grayArrowRight.pdf new file mode 100644 index 0000000..472df1d Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/grayArrowRight.imageset/grayArrowRight.pdf differ diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/Contents.json b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/Contents.json new file mode 100644 index 0000000..c29f367 --- /dev/null +++ b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "helpButton.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/helpButton.pdf b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/helpButton.pdf new file mode 100644 index 0000000..59df160 Binary files /dev/null and b/Samples/PagingLayoutSamples/Assets.xcassets/Shapes/helpButton.imageset/helpButton.pdf differ 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/Catalyst.swift b/Samples/PagingLayoutSamples/Catalyst.swift new file mode 100644 index 0000000..4d43017 --- /dev/null +++ b/Samples/PagingLayoutSamples/Catalyst.swift @@ -0,0 +1,26 @@ +// +// Catalyst.swift +// PagingLayoutSamples +// +// Created by Amir on 05/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation + +enum Catalyst { + static var bridge: AppKitBridge? = { + #if targetEnvironment(macCatalyst) + guard let url = Bundle.main.builtInPlugInsURL?.appendingPathComponent("AppKitGlue.bundle"), + let bundle = Bundle(path: url.path), + bundle.load() else { + return nil + } + guard let principalClass = bundle.principalClass as? NSObject.Type else { return nil } + guard let appKit = principalClass.init() as? AppKitBridge else { return nil } + return appKit + #else + return nil + #endif + }() +} 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 4ab5876..11347b9 100644 --- a/Samples/PagingLayoutSamples/Info.plist +++ b/Samples/PagingLayoutSamples/Info.plist @@ -15,9 +15,11 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) + LSApplicationCategoryType + public.app-category.developer-tools LSRequiresIPhoneOS UILaunchStoryboardName @@ -26,16 +28,17 @@ armv7 + UIRequiresFullScreen + UIStatusBarStyle UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UISupportedInterfaceOrientations~ipad - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/Samples/PagingLayoutSamples/Models/LayoutDesignerIntroInfo.swift b/Samples/PagingLayoutSamples/Models/LayoutDesignerIntroInfo.swift new file mode 100644 index 0000000..174e68a --- /dev/null +++ b/Samples/PagingLayoutSamples/Models/LayoutDesignerIntroInfo.swift @@ -0,0 +1,18 @@ +// +// LayoutDesignerIntroInfo.swift +// PagingLayoutSamples +// +// Created by Amir on 09/07/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation + +struct LayoutDesignerIntroInfo { + let title: String + let headerImageName: String? + let imageName: String? + let description: String + var leftButtonTitle: String + let rightButtonTitle: String +} 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/Models/ShapeLayout.swift b/Samples/PagingLayoutSamples/Models/ShapeLayout.swift index 4506b7f..077fbf4 100644 --- a/Samples/PagingLayoutSamples/Models/ShapeLayout.swift +++ b/Samples/PagingLayoutSamples/Models/ShapeLayout.swift @@ -7,6 +7,7 @@ // import UIKit +import CollectionViewPagingLayout enum ShapeLayout { case scaleInvertedCylinder @@ -34,3 +35,42 @@ enum ShapeLayout { case snapshotPuzzle case snapshotFade } + +extension ShapeLayout { + static let scaleLayouts: [ShapeLayout] = [ + .scaleInvertedCylinder, + .scaleCylinder, + .scaleCoverFlow, + .scaleRotary, + .scaleLinear, + .scaleEaseIn, + .scaleEaseOut, + .scaleBlur + ] + + static let stackLayouts: [ShapeLayout] = [ + .stackVortex, + .stackRotary, + .stackTransparent, + .stackBlur, + .stackReverse, + .stackPerspective + ] + + static let snapshotLayouts: [ShapeLayout] = [ + .snapshotBars, + .snapshotFade, + .snapshotGrid, + .snapshotChess, + .snapshotLines, + .snapshotSpace, + .snapshotTiles, + .snapshotPuzzle + ] +} + +extension Array where Element == ShapeLayout { + static var scale: [ShapeLayout] { ShapeLayout.scaleLayouts } + static var stack: [ShapeLayout] { ShapeLayout.stackLayouts } + static var snapshot: [ShapeLayout] { ShapeLayout.snapshotLayouts } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift new file mode 100644 index 0000000..4ff69f0 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.swift @@ -0,0 +1,133 @@ +// +// LayoutDesignerCodePreviewViewController.swift +// PagingLayoutSamples +// +// Created by Amir on 26/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import UIKit +import Splash + +protocol LayoutDesignerCodePreviewViewControllerDelegate: AnyObject { + func layoutDesignerCodePreviewViewController(_ vc: LayoutDesignerCodePreviewViewController, onHelpButtonTouched button: UIButton) +} + + +class LayoutDesignerCodePreviewViewController: UIViewController, NibBased, ViewModelBased { + + // MARK: Properties + + var viewModel: LayoutDesignerCodePreviewViewModel! { + didSet { + refreshViews() + } + } + weak var delegate: LayoutDesignerCodePreviewViewControllerDelegate? + + @IBOutlet private weak var codeTextView: UITextView! + @IBOutlet private weak var copyButton: UIButton! + @IBOutlet private weak var saveButton: UIButton! + @IBOutlet private weak var helpButton: UIButton! + @IBOutlet private weak var codeModeSegmentedControl: UISegmentedControl! + + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + } + + + // MARK: Listener + + @IBAction private func copyButtonTouched() { + let pasteBoard = UIPasteboard.general + pasteBoard.string = codeTextView.text + } + + @IBAction private func saveButtonTouched() { + guard let exportURL = viewModel.sampleProjectTempURL else { return } + viewModel.generateSampleProject(type: selectedCodeType()) + let controller = UIDocumentPickerViewController(forExporting: [exportURL]) + controller.delegate = self + present(controller, animated: true) + } + + @IBAction private func onHelpButtonTouched() { + delegate?.layoutDesignerCodePreviewViewController(self, onHelpButtonTouched: helpButton) + } + + @IBAction private func codeTypeChanged() { + refreshViews() + } + + + // MARK: Private functions + + private func configureViews() { + configureTextView() + configureButtons() + configureCodeTypeSegmentedControl() + } + + private func configureButtons() { + [saveButton, copyButton, helpButton].forEach { + $0?.layer.cornerRadius = 8 + } + } + + private func configureCodeTypeSegmentedControl() { + codeModeSegmentedControl.backgroundColor = UIColor.black.withAlphaComponent(0.4) + codeModeSegmentedControl.selectedSegmentTintColor = UIColor.white.withAlphaComponent(0.4) + 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(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 + } + +} + + +extension LayoutDesignerCodePreviewViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + 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 new file mode 100644 index 0000000..1d8671d --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewController.xib @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift new file mode 100644 index 0000000..ba86f68 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Code/LayoutDesignerCodePreviewViewModel.swift @@ -0,0 +1,278 @@ +// +// LayoutDesignerCodePreviewViewModel.swift +// PagingLayoutSamples +// +// Created by Amir on 26/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +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: 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(type: CodeType) -> NSAttributedString { + highlighter.highlight(getCode(type: type)) + } + + 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 + } + 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 + + 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"), + to: sampleProjectTempURL.appendingPathComponent("PagingLayout.xcodeproj")) + + let projectURL = sampleProjectTempURL.appendingPathComponent("PagingLayout.xcodeproj") + + try? FileManager.default.moveItem(at: projectURL.appendingPathComponent("project.pbxproj_sample"), + to: projectURL.appendingPathComponent("project.pbxproj")) + + try? FileManager.default.moveItem(at: projectURL.appendingPathComponent("project.xcworkspace_sample"), + to: projectURL.appendingPathComponent("project.xcworkspace")) + + try? FileManager.default.moveItem(at: baseProjectPath.appendingPathComponent("info_sample.plist"), + to: baseProjectPath.appendingPathComponent("info.plist")) + } + + func removeSampleProject() { + guard let sampleProjectTempURL = sampleProjectTempURL else { + return + } + try? FileManager.default.removeItem(at: sampleProjectTempURL) + } + + + // MARK: Private functions + + 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) } ?? "" + + return """ + import UIKit + + // Make sure you added this dependency to your project + // More info at https://bit.ly/CVPagingLayout + import CollectionViewPagingLayout + + // The cell class needs to conform to `\(viewProtocolName)` protocol + // to be able to provide the transform options + class MyCell: UICollectionViewCell, \(viewProtocolName) { + + \(code.replacingOccurrences(of: "\n", with: "\n ")) + + // 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) + } + } + + // 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 + ) + } + + } + """ + } + +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.swift new file mode 100644 index 0000000..ac161ef --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.swift @@ -0,0 +1,123 @@ +// +// LayoutDesignerIntroCell.swift +// PagingLayoutSamples +// +// Created by Amir on 09/07/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit +import CollectionViewPagingLayout + +protocol LayoutDesignerIntroCellDelegate: AnyObject { + func layoutDesignerIntroCell(_ cell: LayoutDesignerIntroCell, onLeftButtonTouched button: UIButton) + func layoutDesignerIntroCell(_ cell: LayoutDesignerIntroCell, onRightButtonTouched button: UIButton) +} + + +class LayoutDesignerIntroCell: UICollectionViewCell, NibBased { + + // MARK: Properties + + var introInfo: LayoutDesignerIntroInfo? { + didSet { + updateViews() + } + } + weak var delegate: LayoutDesignerIntroCellDelegate? + + @IBOutlet private weak var containerView: UIView! + @IBOutlet private weak var stackView: UIStackView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var headerImageView: UIImageView! + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var leftButton: UIButton! + @IBOutlet private weak var rightButton: UIButton! + @IBOutlet private weak var descriptionLabel: UILabel! + + + // MARK: Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + setupViews() + } + + + // MARK: Listeners + + @IBAction private func onLeftButtonTouched() { + delegate?.layoutDesignerIntroCell(self, onLeftButtonTouched: leftButton) + } + + @IBAction private func onRightButtonTouched() { + delegate?.layoutDesignerIntroCell(self, onRightButtonTouched: rightButton) + } + + + // MARK: Private functions + + private func setupViews() { + + } + + private func updateViews() { + guard let info = introInfo else { return } + titleLabel.text = info.title + if let headerImageName = info.headerImageName { + headerImageView.isHidden = false + headerImageView.image = UIImage(named: headerImageName) + } else { + headerImageView.isHidden = true + } + if let imageName = info.imageName { + imageView.isHidden = false + imageView.image = UIImage(named: imageName) + } else { + imageView.isHidden = true + } + descriptionLabel.text = info.description + + leftButton.setTitle(info.leftButtonTitle, for: .normal) + rightButton.setTitle(info.rightButtonTitle, for: .normal) + } + +} + + +extension LayoutDesignerIntroCell: ScaleTransformView { + + var scaleOptions: ScaleTransformViewOptions { + ScaleTransformViewOptions( + minScale: 0.00, + maxScale: 1.35, + scaleRatio: 0.39, + translationRatio: .init(x: 0.10, y: 0.10), + minTranslationRatio: .init(x: -1.00, y: 0.00), + maxTranslationRatio: .init(x: 1.00, y: 1.00), + keepVerticalSpacingEqual: true, + keepHorizontalSpacingEqual: true, + scaleCurve: .linear, + translationCurve: .linear, + shadowEnabled: false, + rotation3d: .init( + angle: 0.60, + minAngle: -1.05, + maxAngle: 1.05, + x: 0.00, + y: 0.00, + z: 1.00, + m34: 0 + ), + translation3d: .init( + translateRatios: (0.90, 0.10, 0.00), + minTranslateRatios: (-3.00, -0.80, -0.30), + maxTranslateRatios: (3.00, 0.80, -0.30) + ) + ) + } + + var scalableView: UIView { + stackView + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.xib b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.xib new file mode 100644 index 0000000..bcc5001 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroCell.xib @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift new file mode 100644 index 0000000..c85f585 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewController.swift @@ -0,0 +1,121 @@ +// +// LayoutDesignerIntroViewController.swift +// PagingLayoutSamples +// +// Created by Amir on 09/07/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit +import CollectionViewPagingLayout + +class LayoutDesignerIntroViewController: UIViewController { + + // MARK: Properties + + var viewModel: LayoutDesignerIntroViewModel? { + didSet { + refreshViews() + } + } + private var collectionView: UICollectionView! + + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .clear + setupCollectionView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + open() + } + + + // MARK: Private functions + + private func setupCollectionView() { + collectionView = UICollectionView(frame: .zero, collectionViewLayout: CollectionViewPagingLayout()) + collectionView.isPagingEnabled = true + collectionView.register(LayoutDesignerIntroCell.self) + collectionView.dataSource = self + collectionView.layer.cornerRadius = 44 + collectionView.clipsToBounds = true + collectionView.backgroundColor = .white + collectionView.isScrollEnabled = false + collectionView.showsHorizontalScrollIndicator = false + collectionView.alpha = 0 + + let darkOverlay = UIView() + darkOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.59) + view.fill(with: darkOverlay) + let tap = UITapGestureRecognizer(target: self, action: #selector(close)) + darkOverlay.addGestureRecognizer(tap) + darkOverlay.alpha = 0 + + view.addSubview(collectionView) + collectionView.widthAnchor.constraint(equalToConstant: 950).isActive = true + collectionView.heightAnchor.constraint(equalToConstant: 600).isActive = true + darkOverlay.center(to: collectionView) + } + + private func refreshViews() { + collectionView?.reloadData() + } + + private func open() { + UIView.animate(withDuration: 0.25) { [weak self] in + self?.view.subviews.forEach { + $0.alpha = 1 + } + } + } + + @objc private func close() { + UIView.animate(withDuration: 0.25, animations: { [weak self] in + self?.view.subviews.forEach { + $0.alpha = 0 + } + }, completion: { [weak self] _ in + self?.view.removeFromSuperview() + self?.didMove(toParent: nil) + }) + } + +} + + +extension LayoutDesignerIntroViewController: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + viewModel?.introPages.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(for: indexPath) as LayoutDesignerIntroCell + cell.introInfo = viewModel?.introPages[indexPath.row] + cell.delegate = self + return cell + } +} + + +extension LayoutDesignerIntroViewController: LayoutDesignerIntroCellDelegate { + func layoutDesignerIntroCell(_ cell: LayoutDesignerIntroCell, onLeftButtonTouched button: UIButton) { + if collectionView.indexPath(for: cell)?.item == 0 { + close() + return + } + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.goToPreviousPage() + } + func layoutDesignerIntroCell(_ cell: LayoutDesignerIntroCell, onRightButtonTouched button: UIButton) { + if collectionView.indexPath(for: cell)?.item == viewModel.map({ $0.introPages.count - 1 }) { + close() + return + } + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.goToNextPage() + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewModel.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewModel.swift new file mode 100644 index 0000000..7cb33e1 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Intro/LayoutDesignerIntroViewModel.swift @@ -0,0 +1,77 @@ +// +// LayoutDesignerIntroViewModel.swift +// PagingLayoutSamples +// +// Created by Amir on 09/07/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation + +struct LayoutDesignerIntroViewModel { + + // MARK: Properties + + let introPages: [LayoutDesignerIntroInfo] +} + + +extension Array where Element == LayoutDesignerIntroInfo { + static var all: [LayoutDesignerIntroInfo] = [ + .init(title: "Welcome to Layout Designer", + headerImageName: "logoForIntro" , + imageName: nil, + description: """ + The easiest way to make a beautiful paging layout for your UICollectionView. + You can use it for your iOS, iPadOS, and macOS Catalyst app. + """, + leftButtonTitle: "Skip", + rightButtonTitle: "Show me how!"), + .init(title: "Select the layout group", + headerImageName: nil, + imageName: "intro01", + description: """ + There are three groups of layouts available. + You can switch between them quickly and see the live preview on the middle panel. + """, + leftButtonTitle: "Previous", + rightButtonTitle: "Next"), + .init(title: "Select the layout type ", + headerImageName: nil, + imageName: "intro02", + description: """ + There are many layouts available for each group. + You can switch between them by clicking on the circles. + If you use a Trackpad you can also switch between them by scrolling to right and left. + """, + leftButtonTitle: "Previous", + rightButtonTitle: "Next"), + .init(title: "Switch between shapes and adjust options ", + headerImageName: nil, + imageName: "intro03", + description: """ + Now you can see the result on the sample cards (orange cards) + You can switch between them to see the animation by clicking on the arrows or on the card itself. + If you use a Trackpad you can also switch between them by scrolling to right and left. + Now adjust the options if needed and see changes in real-time. + """, + leftButtonTitle: "Previous", + rightButtonTitle: "Next"), + .init(title: "That’s it! your code is ready to use!", + headerImageName: nil, + imageName: "intro04", + description: """ + You can copy the generated code and use it in your project + If you need to see how to use the code try “Save as Project” and open it with Xcode + """, + leftButtonTitle: "Previous", + rightButtonTitle: "Design Layout!") + ] + + + static var allExceptWelcome: [LayoutDesignerIntroInfo] { + var list = Array(all.dropFirst()) + list[0].leftButtonTitle = "Close" + return list + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift new file mode 100644 index 0000000..9fcc358 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.swift @@ -0,0 +1,184 @@ +// +// LayoutDesignerViewController.swift +// CollectionViewPagingLayout +// +// Created by Amir on 05/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit +import Splash + +class LayoutDesignerViewController: UIViewController, ViewModelBased, NibBased { + + // MARK: Properties + + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } + + // MARK: Properties + + var viewModel: LayoutDesignerViewModel! + + @IBOutlet private weak var stackButtonView: UIView! + @IBOutlet private weak var scaleButtonView: UIView! + @IBOutlet private weak var snapshotButtonView: UIView! + @IBOutlet private weak var previewContainerView: UIView! + @IBOutlet private weak var codeContainerView: UIView! + @IBOutlet private weak var optionsTableView: LayoutDesignerOptionsTableView! + + private var previewViewController: ShapesViewController! + private var codePreviewViewController: LayoutDesignerCodePreviewViewController! + + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + setOptionsList() + registerKeyboardNotifications() + + view.backgroundColor = .clear + codeContainerView.backgroundColor = codeContainerView.backgroundColor?.withAlphaComponent(0.6) + } + + + // MARK: Event listeners + + @IBAction private func layoutCategoryButtonTouched(button: UIButton) { + guard let view = button.superview else { return } + + switch view { + case stackButtonView: + viewModel.layouts = .stack + case scaleButtonView: + viewModel.layouts = .scale + case snapshotButtonView: + viewModel.layouts = .snapshot + default: + viewModel.layouts = [] + } + previewViewController.viewModel = viewModel.shapesViewModel + + setLayoutButtonSelected(view: stackButtonView, isSelected: view == stackButtonView) + setLayoutButtonSelected(view: scaleButtonView, isSelected: view == scaleButtonView) + setLayoutButtonSelected(view: snapshotButtonView, isSelected: view == snapshotButtonView) + + UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1, options: .curveEaseOut, animations: { + view.superview?.layoutIfNeeded() + }, completion: nil) + } + + + // MARK: Private functions + + private func configureViews() { + setInitialStateForLayoutButtons() + addPreviewController() + addCodePreviewController() + } + + private func setInitialStateForLayoutButtons() { + setLayoutButtonSelected(view: stackButtonView, isSelected: false) + setLayoutButtonSelected(view: scaleButtonView, isSelected: true) + setLayoutButtonSelected(view: snapshotButtonView, isSelected: false) + } + + private func setLayoutButtonSelected(view: UIView, isSelected: Bool, animated: Bool = true) { + guard let titleStackView = view.subviews.first(where: { $0 is UIStackView })? + .subviews.first(where: { $0 is UIStackView }) else { + return + } + titleStackView.isHidden = !isSelected + + let borderColor = UIColor.white.withAlphaComponent(0.66).cgColor + let noBorderColor = UIColor.clear.cgColor + + let oldBorderColor = view.layer.borderColor + view.layer.borderColor = isSelected ? borderColor : noBorderColor + + guard animated else { + return + } + let borderAnimation = CABasicAnimation(keyPath: "borderColor") + borderAnimation.duration = 0.4 + borderAnimation.fromValue = oldBorderColor + borderAnimation.toValue = view.layer.borderColor + view.layer.add(borderAnimation, forKey: nil) + } + + private func addPreviewController() { + previewViewController = ShapesViewController.instantiate(viewModel: viewModel.shapesViewModel) + addChild(previewViewController) + previewViewController.view.layer.cornerRadius = 30 + previewViewController.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + previewContainerView.fill(with: previewViewController.view) + previewViewController.didMove(toParent: self) + previewViewController.delegate = self + viewModel.onOptionsChange = { [weak self] in + self?.previewViewController.reloadAndInvalidateShapes() + } + } + + private func addCodePreviewController() { + codePreviewViewController = LayoutDesignerCodePreviewViewController.instantiate() + codePreviewViewController.delegate = self + addChild(codePreviewViewController) + codeContainerView.fill(with: codePreviewViewController.view) + codePreviewViewController.didMove(toParent: self) + viewModel.onCodePreviewViewModelChange = { [weak self] in + self?.codePreviewViewController.viewModel = $0 + } + } + + private func setOptionsList() { + optionsTableView.optionViewModels = viewModel.optionViewModels + } + + private func showIntroViewController(viewModel: LayoutDesignerIntroViewModel) { + let vc = LayoutDesignerIntroViewController() + vc.viewModel = viewModel + addChild(vc) + view.fill(with: vc.view) + vc.didMove(toParent: self) + } + + private func registerKeyboardNotifications() { + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + } + + @objc private func adjustForKeyboard(notification: Notification) { + guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + let keyboardScreenEndFrame = keyboardValue.cgRectValue + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) + var contentInset = optionsTableView.contentInset + + if keyboardViewEndFrame.minY < optionsTableView.frame.maxY { + contentInset.bottom = optionsTableView.frame.maxY - keyboardViewEndFrame.minY - 8 + } else { + contentInset.bottom = 8 + } + optionsTableView.contentInset = contentInset + } + +} + + +extension LayoutDesignerViewController: ShapesViewControllerDelegate { + func shapesViewController(_ vc: ShapesViewController, onSelectedLayoutChange layout: ShapeLayout) { + viewModel.selectedLayout = layout + setOptionsList() + } +} + + +extension LayoutDesignerViewController: LayoutDesignerCodePreviewViewControllerDelegate { + func layoutDesignerCodePreviewViewController(_ vc: LayoutDesignerCodePreviewViewController, onHelpButtonTouched button: UIButton) { + showIntroViewController(viewModel: viewModel.getIntroViewModel(showWelcome: false)) + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib new file mode 100644 index 0000000..e3ec631 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewController.xib @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewModel.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewModel.swift new file mode 100644 index 0000000..3f163e2 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/LayoutDesignerViewModel.swift @@ -0,0 +1,357 @@ +// +// LayoutDesignerViewModel.swift +// PagingLayoutSamples +// +// Created by Amir on 27/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import CollectionViewPagingLayout + +class LayoutDesignerViewModel { + + // MARK: Properties + + var onCodePreviewViewModelChange: ((LayoutDesignerCodePreviewViewModel) -> Void)? + var onOptionsChange: (() -> Void)? + var selectedLayout: ShapeLayout? { + didSet { + refreshOptionViewModels() + } + } + var layouts: [ShapeLayout] = .scale + var shapesViewModel: ShapesViewModel { + ShapesViewModel(layouts: layouts, showBackButton: false, showPageControl: true) + } + + + private(set) var optionViewModels: [LayoutDesignerOptionSectionViewModel] = [] + private let codeGenerator = OptionsCodeGenerator() + + + // MARK: Public functions + + func getIntroViewModel(showWelcome: Bool = true) -> LayoutDesignerIntroViewModel { + LayoutDesignerIntroViewModel(introPages: showWelcome ? .all : .allExceptWelcome) + } + + + // MARK: Private functions + + private func updateCodePreview(options: T) { + onCodePreviewViewModelChange?(.init(code: codeGenerator.generateCode(options: options))) + } + private func update(options: inout T, closure: (inout T) -> Void) { + closure(&options) + updateCodePreview(options: options) + shapesViewModel.setCustomOptions(options) + onOptionsChange?() + } + + private func refreshOptionViewModels() { + guard let selectedLayout = selectedLayout else { + optionViewModels = [] + return + } + + if let options = selectedLayout.scaleOptions { + optionViewModels = getOptionViewModels(scaleOptions: options) + } else if let options = selectedLayout.stackOptions { + optionViewModels = getOptionViewModels(stackOptions: options) + } else if let options = selectedLayout.snapshotOptions { + optionViewModels = getOptionViewModels(snapshotOptions: options) + } + } + + private func getOptionViewModels(scaleOptions: ScaleTransformViewOptions) -> [LayoutDesignerOptionSectionViewModel] { + var options = scaleOptions + updateCodePreview(options: options) + let update: ((inout ScaleTransformViewOptions) -> Void) -> Void = { [weak self] in + self?.update(options: &options, closure: $0) + } + + let generalOptions: [LayoutDesignerOptionViewModel] = [ + .init(title: "Min scale", kind: .singleSlider(current: options.minScale, range: 0...2) { n in + update { $0.minScale = n! } + }), + .init(title: "Max scale", kind: .singleSlider(current: options.maxScale, range: 0...2) { n in + update { $0.maxScale = n! } + }), + .init(title: "Scale ratio", kind: .singleSlider(current: options.scaleRatio, range: 0...2) { n in + update { $0.scaleRatio = n! } + }), + .init(title: "Translation ratio", kind: .doubleSlider(current: options.translationRatio.pair, range: -2...2) { n in + update { $0.translationRatio = .by(pair: n!) } + }), + .init(title: "Min translation ratio", kind: .doubleSlider(current: options.minTranslationRatio?.pair, range: -5...5, optional: true) { n in + update { $0.minTranslationRatio = n.map { .by(pair: $0) } } + }), + .init(title: "Max translation ratio", kind: .doubleSlider(current: options.maxTranslationRatio?.pair, range: -5...5, optional: true) { n in + update { $0.maxTranslationRatio = n.map { .by(pair: $0) } } + }), + .init(title: "Keep vertical spacing equal", kind: .toggleSwitch(current: options.keepVerticalSpacingEqual) { n in + update { $0.keepVerticalSpacingEqual = n } + }), + .init(title: "Keep horizontal spacing equal", kind: .toggleSwitch(current: options.keepHorizontalSpacingEqual) { n in + update { $0.keepHorizontalSpacingEqual = n } + }), + .init(title: "Scale curve", kind: .segmented(options: TransformCurve.all.map(\.name), current: options.scaleCurve.name) { n in + update { $0.scaleCurve = .by(name: n)! } + }), + .init(title: "Translation curve", kind: .segmented(options: TransformCurve.all.map(\.name), current: options.translationCurve.name) { n in + update { $0.translationCurve = .by(name: n)! } + }), + .init(title: "Shadow enabled", kind: .toggleSwitch(current: options.shadowEnabled) { n in + update { $0.shadowEnabled = n } + }), + .init(title: "Shadow opacity", kind: .singleSlider(current: CGFloat(options.shadowOpacity)) { n in + update { $0.shadowOpacity = Float(n!) } + }), + .init(title: "Shadow opacity min", kind: .singleSlider(current: CGFloat(options.shadowOpacityMin)) { n in + update { $0.shadowOpacityMin = Float(n!) } + }), + .init(title: "Shadow opacity max", kind: .singleSlider(current: CGFloat(options.shadowOpacityMax)) { n in + update { $0.shadowOpacityMax = Float(n!) } + }), + .init(title: "Shadow radius max", kind: .singleSlider(current: options.shadowRadiusMax, range: 0...15) { n in + update { $0.shadowRadiusMax = n! } + }), + .init(title: "Shadow radius min", kind: .singleSlider(current: options.shadowRadiusMin, range: 0...15) { n in + update { $0.shadowRadiusMin = n! } + }), + .init(title: "Shadow offset min", kind: .doubleSlider(current: options.shadowOffsetMin.pair, range: -7...7) { n in + update { $0.shadowOffsetMin = .by(pair: n!) } + }), + .init(title: "Shadow offset max", kind: .doubleSlider(current: options.shadowOffsetMax.pair, range: -7...7) { n in + update { $0.shadowOffsetMax = .by(pair: n!) } + }), + .init(title: "Blur effect enabled", kind: .toggleSwitch(current: options.blurEffectEnabled) { n in + update { $0.blurEffectEnabled = n } + }), + .init(title: "Blur effect radius ratio", kind: .singleSlider(current: options.blurEffectRadiusRatio) { n in + update { $0.blurEffectRadiusRatio = n! } + }), + .init(title: "Blur effect style", kind: .segmented(options: UIBlurEffect.Style.all.map(\.name), current: options.blurEffectStyle.name) { n in + update { $0.blurEffectStyle = .by(name: n)! } + }) + ] + + let originalRotation3dOptions = options.rotation3d ?? ScaleTransformViewOptions.Rotation3dOptions( + angle: .pi / 3, + minAngle: 0, + maxAngle: .pi, + x: 0, + y: 1, + z: 0, + m34: -0.000_1 + ) + + let rotation3dOptions: [LayoutDesignerOptionViewModel] = [ + .init(title: "Enabled", kind: .toggleSwitch(current: options.rotation3d != nil) { n in + update { $0.rotation3d = !n ? nil : originalRotation3dOptions } + }), + .init(title: "Angle", kind: .singleSlider(current: options.rotation3d?.angle, range: -.pi...CGFloat.pi) { n in + update { $0.rotation3d?.angle = n! } + }), + .init(title: "Min angle", kind: .singleSlider(current: options.rotation3d?.minAngle, range: -.pi...CGFloat.pi) { n in + update { $0.rotation3d?.minAngle = n! } + }), + .init(title: "Max angle", kind: .singleSlider(current: options.rotation3d?.maxAngle, range: -.pi...CGFloat.pi) { n in + update { $0.rotation3d?.maxAngle = n! } + }), + .init(title: "X", kind: .singleSlider(current: options.rotation3d?.x, range: -1...1) { n in + update { $0.rotation3d?.x = n! } + }), + .init(title: "Y", kind: .singleSlider(current: options.rotation3d?.y, range: -1...1) { n in + update { $0.rotation3d?.y = n! } + }), + .init(title: "Z", kind: .singleSlider(current: options.rotation3d?.z, range: -1...1) { n in + update { $0.rotation3d?.z = n! } + }), + .init(title: "m34", kind: .singleSlider(current: options.rotation3d.map { $0.m34 * 1_000 }, range: -2...2) { n in + update { $0.rotation3d?.m34 = n! / 1_000 } + }) + ] + + let originalTranslation3dOptions = options.translation3d ?? ScaleTransformViewOptions.Translation3dOptions( + translateRatios: (0.1, 0, 0), + minTranslateRatios: (-0.05, 0, 0.86), + maxTranslateRatios: (0.05, 0, -0.86) + ) + + let translation3dOptions: [LayoutDesignerOptionViewModel] = [ + .init(title: "Enabled", kind: .toggleSwitch(current: options.translation3d != nil) { n in + update { $0.translation3d = !n ? nil : originalTranslation3dOptions } + }), + .init(title: "X ratio", kind: .singleSlider(current: options.translation3d?.translateRatios.0, range: -5...5) { n in + update { + guard let current = $0.translation3d?.translateRatios else { return } + $0.translation3d?.translateRatios = (n!, current.1, current.2) + } + }), + .init(title: "X min ratio", kind: .singleSlider(current: options.translation3d?.minTranslateRatios.0, range: -10...10) { n in + update { + guard let current = $0.translation3d?.minTranslateRatios else { return } + $0.translation3d?.minTranslateRatios = (n!, current.1, current.2) + } + }), + .init(title: "X max ratio", kind: .singleSlider(current: options.translation3d?.maxTranslateRatios.0, range: -10...10) { n in + update { + guard let current = $0.translation3d?.maxTranslateRatios else { return } + $0.translation3d?.maxTranslateRatios = (n!, current.1, current.2) + } + }), + .init(title: "Y ratio", kind: .singleSlider(current: options.translation3d?.translateRatios.1, range: -5...5) { n in + update { + guard let current = $0.translation3d?.translateRatios else { return } + $0.translation3d?.translateRatios = (current.0, n!, current.2) + } + }), + .init(title: "Y min ratio", kind: .singleSlider(current: options.translation3d?.minTranslateRatios.1, range: -10...10) { n in + update { + guard let current = $0.translation3d?.minTranslateRatios else { return } + $0.translation3d?.minTranslateRatios = (current.0, n!, current.2) + } + }), + .init(title: "Y max ratio", kind: .singleSlider(current: options.translation3d?.maxTranslateRatios.1, range: -10...10) { n in + update { + guard let current = $0.translation3d?.maxTranslateRatios else { return } + $0.translation3d?.maxTranslateRatios = (current.0, n!, current.2) + } + }), + .init(title: "Z ratio", kind: .singleSlider(current: options.translation3d?.translateRatios.2, range: -5...5) { n in + update { + guard let current = $0.translation3d?.translateRatios else { return } + $0.translation3d?.translateRatios = (current.0, current.1, n!) + } + }), + .init(title: "Z min ratio", kind: .singleSlider(current: options.translation3d?.minTranslateRatios.2, range: -10...10) { n in + update { + guard let current = $0.translation3d?.minTranslateRatios else { return } + $0.translation3d?.minTranslateRatios = (current.0, current.1, n!) + } + }), + .init(title: "Z max ratio", kind: .singleSlider(current: options.translation3d?.maxTranslateRatios.2, range: -10...10) { n in + update { + guard let current = $0.translation3d?.maxTranslateRatios else { return } + $0.translation3d?.maxTranslateRatios = (current.0, current.1, n!) + } + }) + ] + + + return [ + .init(title: "Options", items: generalOptions), + .init(title: "Rotation 3D options", items: rotation3dOptions), + .init(title: "Translation 3D options", items: translation3dOptions) + ] + } + + private func getOptionViewModels(stackOptions: StackTransformViewOptions) -> [LayoutDesignerOptionSectionViewModel] { + var options = stackOptions + updateCodePreview(options: options) + let update: ((inout StackTransformViewOptions) -> Void) -> Void = { [weak self] in + self?.update(options: &options, closure: $0) + } + + let viewModels: [LayoutDesignerOptionViewModel] = [ + .init(title: "Scale factor", kind: .singleSlider(current: options.scaleFactor, range: -1...1) { n in + update { $0.scaleFactor = n! } + }), + .init(title: "Min scale", kind: .singleSlider(current: options.minScale, optional: true) { n in + update { $0.minScale = n } + }), + .init(title: "Max scale", kind: .singleSlider(current: options.maxScale, optional: true) { n in + update { $0.maxScale = n } + }), + .init(title: "Spacing factor", kind: .singleSlider(current: options.spacingFactor, range: 0...0.5) { n in + update { $0.spacingFactor = n! } + }), + .init(title: "Max spacing", kind: .singleSlider(current: options.maxSpacing, optional: true) { n in + update { $0.maxSpacing = n } + }), + .init(title: "Alpha factor", kind: .singleSlider(current: options.alphaFactor) { n in + update { $0.alphaFactor = n! } + }), + .init(title: "Bottom stack alpha speed factor", kind: .singleSlider(current: options.bottomStackAlphaSpeedFactor, range: 0...10) { n in + update { $0.bottomStackAlphaSpeedFactor = n! } + }), + .init(title: "Top stack alpha speed factor", kind: .singleSlider(current: options.topStackAlphaSpeedFactor, range: 0...10) { n in + update { $0.topStackAlphaSpeedFactor = n! } + }), + .init(title: "Perspective ratio", kind: .singleSlider(current: options.perspectiveRatio, range: -1...1) { n in + update { $0.perspectiveRatio = n! } + }), + .init(title: "Shadow enabled", kind: .toggleSwitch(current: options.shadowEnabled) { n in + update { $0.shadowEnabled = n } + }), + .init(title: "Shadow opacity", kind: .singleSlider(current: CGFloat(options.shadowOpacity)) { n in + update { $0.shadowOpacity = Float(n!) } + }), + .init(title: "Shadow offset", kind: .doubleSlider(current: options.shadowOffset.pair) { n in + update { $0.shadowOffset = .by(pair: n!) } + }), + .init(title: "Shadow radius", kind: .singleSlider(current: options.shadowRadius, range: 1...10) { n in + update { $0.shadowRadius = n! } + }), + .init(title: "Rotate angel", kind: .singleSlider(current: options.stackRotateAngel, range: -CGFloat.pi...CGFloat.pi) { n in + update { $0.stackRotateAngel = n! } + }), + .init(title: "Pop angle", kind: .singleSlider(current: options.popAngle, range: -CGFloat.pi...CGFloat.pi) { n in + update { $0.popAngle = n! } + }), + .init(title: "Pop offset ratio", kind: .doubleSlider(current: options.popOffsetRatio.pair, range: -2...2) { n in + update { $0.popOffsetRatio = .by(pair: n!) } + }), + .init(title: "Stack position", kind: .doubleSlider(current: options.stackPosition.pair, range: -1...1) { n in + update { $0.stackPosition = .by(pair: n!) } + }), + .init(title: "Reverse", kind: .toggleSwitch(current: options.reverse) { n in + update { $0.reverse = n } + }), + .init(title: "Blur effect enabled", kind: .toggleSwitch(current: options.blurEffectEnabled) { n in + update { $0.blurEffectEnabled = n } + }), + .init(title: "Max blur radius", kind: .singleSlider(current: options.maxBlurEffectRadius) { n in + update { $0.maxBlurEffectRadius = n! } + }), + .init(title: "Blur effect style", kind: .segmented(options: UIBlurEffect.Style.all.map(\.name), current: options.blurEffectStyle.name) { n in + update { $0.blurEffectStyle = .by(name: n)! } + }) + ] + + return [ + .init(title: "Options", items: viewModels) + ] + } + + private func getOptionViewModels(snapshotOptions: SnapshotTransformViewOptions) -> [LayoutDesignerOptionSectionViewModel] { + var options = snapshotOptions + updateCodePreview(options: options) + + let update: ((inout SnapshotTransformViewOptions) -> Void) -> Void = { [weak self] in + self?.update(options: &options, closure: $0) + } + let viewModels: [LayoutDesignerOptionViewModel] = [ + .init(title: "Piece size ratio", kind: .doubleSlider(current: options.pieceSizeRatio.pair, range: 0.01...1) { n in + update { $0.pieceSizeRatio = .by(pair: n!) } + }), + .init(title: "Container scale ratio", kind: .singleSlider(current: options.containerScaleRatio) { n in + update { $0.containerScaleRatio = n! } + }), + .init(title: "Container translation ratio", kind: .doubleSlider(current: options.containerTranslationRatio.pair, range: 0...2) { n in + update { $0.containerTranslationRatio = .by(pair: n!) } + }), + .init(title: "Container min translation ratio", kind: .doubleSlider(current: options.containerMinTranslationRatio?.pair, range: 0...2, optional: true) { n in + update { $0.containerMinTranslationRatio = n.map { .by(pair: $0) } } + }), + .init(title: "Container max translation ratio", kind: .doubleSlider(current: options.containerMaxTranslationRatio?.pair, range: 0...2, optional: true) { n in + update { $0.containerMaxTranslationRatio = n.map { .by(pair: $0) } } + }) + ] + return [ + .init(title: "Options", items: viewModels) + ] + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/LayoutDesignerOptionsTableView.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/LayoutDesignerOptionsTableView.swift new file mode 100644 index 0000000..8f17d70 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/LayoutDesignerOptionsTableView.swift @@ -0,0 +1,75 @@ +// +// LayoutDesignerOptionsTableView.swift +// PagingLayoutSamples +// +// Created by Amir on 21/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit + +class LayoutDesignerOptionsTableView: UITableView { + + // MARK: Properties + + var optionViewModels: [LayoutDesignerOptionSectionViewModel] = [] { + didSet { + reloadData() + } + } + + + // MARK: Lifecycle + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + + // MARK: Private functions + + private func configure() { + dataSource = self + delegate = self + register(LayoutDesignerOptionCell.self) + backgroundColor = .clear + separatorStyle = .none + allowsSelection = false + sectionHeaderHeight = 40 + } + +} + + +extension LayoutDesignerOptionsTableView: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + optionViewModels.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + optionViewModels[section].items.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: LayoutDesignerOptionCell = tableView.dequeueReusableCell(for: indexPath) + cell.viewModel = optionViewModels[indexPath.section].items[indexPath.row] + return cell + } +} + +extension LayoutDesignerOptionsTableView: UITableViewDelegate { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let header = UIView() + let label = UILabel() + label.textColor = .white + label.adjustsFontSizeToFitWidth = true + label.font = .systemFont(ofSize: 22, weight: .medium) + label.text = optionViewModels[section].title + header.fill(with: label, edges: .init(top: 5, left: 24, bottom: -3, right: -24)) + header.backgroundColor = #colorLiteral(red: 0.1607843137, green: 0.2705882353, blue: 0.8431372549, alpha: 1) + return header + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.swift new file mode 100644 index 0000000..a4c98da --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.swift @@ -0,0 +1,256 @@ +// +// LayoutDesignerOptionCell.swift +// PagingLayoutSamples +// +// Created by Amir on 19/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit + +class LayoutDesignerOptionCell: UITableViewCell, NibBased { + + // MARK: Properties + + var viewModel: LayoutDesignerOptionViewModel? { + didSet { + updateViews() + } + } + + @IBOutlet private weak var label: UILabel! + @IBOutlet private weak var singleSlider: UISlider! + @IBOutlet private weak var singleSliderInput: UITextField! + @IBOutlet private weak var doubleSliderStackView: UIStackView! + @IBOutlet private weak var doubleSlider1: UISlider! + @IBOutlet private weak var doubleSliderInput1: UITextField! + @IBOutlet private weak var doubleSlider2: UISlider! + @IBOutlet private weak var doubleSliderInput2: UITextField! + @IBOutlet private weak var segmentedControl: UISegmentedControl! + @IBOutlet private weak var nilLabel: UILabel! + @IBOutlet private weak var switchView: UISwitch! + + private var onSingleSliderChange: (CGFloat) -> Void = { _ in } + private var onSingleSliderInputChange: (CGFloat) -> Void = { _ in } + private var onDoubleSlider1Change: (CGFloat) -> Void = { _ in } + private var onDoubleSliderInput1Change: (CGFloat) -> Void = { _ in } + private var onDoubleSlider2Change: (CGFloat) -> Void = { _ in } + private var onDoubleSliderInput2Change: (CGFloat) -> Void = { _ in } + private var onSwitchChange: (Bool) -> Void = { _ in } + private var onSelectedSegmentChange: (Int) -> Void = { _ in } + + + // MARK: Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + setupViews() + updateViews() + } + + + // MARK: Private functions + + private func setupViews() { + backgroundColor = .clear + label.textColor = UIColor.white.withAlphaComponent(0.7) + label.adjustsFontSizeToFitWidth = true + nilLabel.textColor = UIColor.white.withAlphaComponent(0.7) + [singleSlider, doubleSlider1, doubleSlider2].forEach { + $0?.tintColor = .white + if UIDevice.current.userInterfaceIdiom != .mac { + $0?.thumbTintColor = .white + $0?.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3) + $0?.minimumTrackTintColor = .white + } + $0?.addTarget(self, action: #selector(onSliderChange(slider:)), for: .valueChanged) + } + [singleSliderInput, doubleSliderInput1, doubleSliderInput2].forEach { + $0?.backgroundColor = .white + $0?.textAlignment = .center + $0?.layer.cornerRadius = 8 + $0?.layer.masksToBounds = true + $0?.addTarget(self, action: #selector(onInputChange(input:)), for: .editingChanged) + $0?.keyboardType = .decimalPad + } + switchView.onTintColor = .systemGreen + switchView.backgroundColor = UIColor.black.withAlphaComponent(0.2) + switchView.layer.cornerRadius = switchView.layer.bounds.height / 2 + switchView.addTarget(self, action: #selector(onSwitchChange(switchView:)), for: .valueChanged) + + segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .normal) + segmentedControl.setTitleTextAttributes([.foregroundColor: #colorLiteral(red: 0.3098039216, green: 0.5215686275, blue: 0.8666666667, alpha: 1)], for: .selected) + segmentedControl.tintColor = .white + segmentedControl.backgroundColor = UIColor.white.withAlphaComponent(0.3) + segmentedControl.addTarget(self, action: #selector(onSelectedSegmentChange(control:)), for: .valueChanged) + } + + @objc private func onSwitchChange(switchView: UISwitch) { + onSwitchChange(switchView.isOn) + } + + @objc private func onSelectedSegmentChange(control: UISegmentedControl) { + onSelectedSegmentChange(control.selectedSegmentIndex) + } + + @objc private func onInputChange(input: UITextField) { + onInputChange(input: input, fromSlider: false) + } + + @objc private func onSliderChange(slider: UISlider) { + onSliderChange(slider: slider, fromInput: false) + } + + private func onSliderChange(slider: UISlider, fromInput: Bool) { + let value = CGFloat(slider.value) + if slider == singleSlider, !(singleSlider.value == Float(value) && fromInput) { + onSingleSliderChange(value) + onInputChange(input: singleSliderInput, fromSlider: true) + singleSliderInput.set(value: value) + } else if slider == doubleSlider1, !(doubleSlider1.value == Float(value) && fromInput) { + onDoubleSlider1Change(value) + onInputChange(input: doubleSliderInput1, fromSlider: true) + doubleSliderInput1.set(value: value) + } else if slider == doubleSlider2, !(doubleSlider2.value == Float(value) && fromInput) { + onDoubleSlider2Change(value) + onInputChange(input: doubleSliderInput2, fromSlider: true) + doubleSliderInput2.set(value: value) + } + } + + private func onInputChange(input: UITextField, fromSlider: Bool) { + let value = input.floatValue ?? 0 + if input == singleSliderInput, !(singleSliderInput.floatValue == value && fromSlider) { + onSingleSliderInputChange(CGFloat(value)) + onSliderChange(slider: singleSlider, fromInput: true) + singleSlider.value = value + } else if input == doubleSliderInput1, !(doubleSliderInput1.floatValue == value && fromSlider) { + onDoubleSliderInput1Change(CGFloat(value)) + onSliderChange(slider: doubleSlider1, fromInput: true) + doubleSlider1.value = value + } else if input == doubleSliderInput2, !(doubleSliderInput2.floatValue == value && fromSlider) { + onDoubleSliderInput2Change(CGFloat(value)) + onSliderChange(slider: doubleSlider2, fromInput: true) + doubleSlider2.value = value + } + } + + private func updateViews() { + guard let viewModel = viewModel, label != nil else { return } + + label.text = viewModel.title + singleSlider.isHidden = true + singleSliderInput.isHidden = true + doubleSliderStackView.isHidden = true + segmentedControl.isHidden = true + switchView.isHidden = true + nilLabel.isHidden = true + + switch viewModel.kind { + + case let .singleSlider(current, range, optional, onChange): + let latestValue = viewModel.getLatestValue() ?? current + singleSlider.isHidden = false + singleSliderInput.isHidden = false + singleSlider.maximumValue = Float(range.upperBound) + singleSlider.minimumValue = Float(range.lowerBound) + singleSliderInput.set(value: latestValue ?? 0) + singleSlider.value = Float(latestValue ?? 0) + let onNewValue: ((CGFloat?) -> Void) = { + viewModel.onNewValue($0) + onChange($0) + } + + onSingleSliderChange = onNewValue + onSingleSliderInputChange = onNewValue + if optional { + switchView.isHidden = false + switchView.isOn = latestValue == nil + nilLabel.isHidden = false + onSwitchChange = { [weak self] in + guard let self = self else { return } + onNewValue($0 ? nil : CGFloat(self.singleSlider.value)) + self.singleSlider.isHidden = $0 + self.singleSliderInput.isHidden = $0 + } + onSwitchChange(switchView.isOn) + } + + case let .doubleSlider(current, range, optional, onChange): + let latestValue = viewModel.getLatestValue() ?? current + doubleSliderStackView.isHidden = false + doubleSliderStackView.alpha = 1 + doubleSlider1.maximumValue = Float(range.upperBound) + doubleSlider2.maximumValue = Float(range.upperBound) + doubleSlider1.minimumValue = Float(range.lowerBound) + doubleSlider2.minimumValue = Float(range.lowerBound) + doubleSliderInput1.set(value: latestValue?.0 ?? 0) + doubleSlider1.value = Float(latestValue?.0 ?? 0) + doubleSliderInput2.set(value: latestValue?.1 ?? 0) + doubleSlider2.value = Float(latestValue?.1 ?? 0) + let getValues = { [weak self] in self.map { (CGFloat($0.doubleSlider1.value), CGFloat($0.doubleSlider2.value)) } } + + let onNewValue: (((CGFloat, CGFloat)?) -> Void) = { + viewModel.onNewValue($0) + onChange($0) + } + + onDoubleSlider1Change = { _ in onNewValue(getValues()) } + onDoubleSlider2Change = { _ in onNewValue(getValues()) } + onDoubleSliderInput1Change = { _ in onNewValue(getValues()) } + onDoubleSliderInput2Change = { _ in onNewValue(getValues()) } + if optional { + switchView.isHidden = false + switchView.isOn = latestValue == nil + nilLabel.isHidden = false + onSwitchChange = { [weak self] in + guard let self = self else { return } + onNewValue($0 ? nil : getValues()) + self.doubleSliderStackView.alpha = $0 ? 0 : 1 + } + onSwitchChange(switchView.isOn) + } + + + case let .toggleSwitch(current, onChange): + switchView.isHidden = false + switchView.isOn = viewModel.getLatestValue() ?? current + onSwitchChange = { + viewModel.onNewValue($0) + onChange($0) + } + + case let .segmented(options, current, onChange): + segmentedControl.isHidden = false + segmentedControl.removeAllSegments() + options.reversed().forEach { + segmentedControl.insertSegment(withTitle: $0, at: 0, animated: false) + } + onSelectedSegmentChange = { + viewModel.onNewValue($0) + onChange(options[$0]) + } + let selected = viewModel.getLatestValue() ?? current + if let index = options.firstIndex(of: selected) { + segmentedControl.selectedSegmentIndex = index + } + } + } + +} + + +private extension UITextField { + + var floatValue: Float? { + text?.floatValue + } + + func set(value: Float) { + set(value: CGFloat(value)) + } + + func set(value: CGFloat) { + text = value.format() + } +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.xib b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.xib new file mode 100644 index 0000000..572d158 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCell.xib @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCellViewModel.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCellViewModel.swift new file mode 100644 index 0000000..a09bc5a --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/Options/cell/LayoutDesignerOptionCellViewModel.swift @@ -0,0 +1,72 @@ +// +// LayoutDesignerOptionCellViewModel.swift +// PagingLayoutSamples +// +// Created by Amir on 19/05/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import UIKit + +class LayoutDesignerOptionSectionViewModel { + + // MARK: Properties + + let title: String + var items: [LayoutDesignerOptionViewModel] + + + // MARK: Lifecycle + + init(title: String, items: [LayoutDesignerOptionViewModel]) { + self.title = title + self.items = items + } +} + + +class LayoutDesignerOptionViewModel { + + // MARK: Constants + + enum Kind { + case singleSlider(current: CGFloat?, range: ClosedRange = 0...1, optional: Bool = false, onChange: (CGFloat?) -> Void) + case doubleSlider(current: (CGFloat, CGFloat)?, range: ClosedRange = 0...1, optional: Bool = false, onChange: ((CGFloat, CGFloat)?) -> Void) + case toggleSwitch(current: Bool, onChange: (Bool) -> Void) + case segmented(options: [String], current: String, onChange: (String) -> Void) + } + + + // MARK: Properties + + let title: String + let kind: Kind + + private var changed: Bool = false + private var latestValue: Any? + + + // MARK: Lifecycle + + init(title: String, kind: Kind) { + self.title = title + self.kind = kind + } + + + // MARK: Public functions + + func onNewValue(_ value: Any?) { + changed = true + latestValue = value + } + + func getLatestValue() -> T? { + if !changed { + return nil + } + return latestValue as? T + } + +} diff --git a/Samples/PagingLayoutSamples/Modules/LayoutDesigner/OptionsCodeGenerator.swift b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/OptionsCodeGenerator.swift new file mode 100644 index 0000000..e5e5419 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/LayoutDesigner/OptionsCodeGenerator.swift @@ -0,0 +1,301 @@ +// +// OptionsCodeGenerator.swift +// PagingLayoutSamples +// +// Created by Amir on 28/06/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import Foundation +import CollectionViewPagingLayout + +class OptionsCodeGenerator { + + // MARK: Public functions + + func generateCode(options: T) -> String { + if let options = options as? ScaleTransformViewOptions { + + let code = generateCode(options: options) + let layoutOptions = ScaleTransformViewOptions.Layout.allCases + .map { (options: ScaleTransformViewOptions.layout($0), layout: $0) } + .first { generateCode(options: $0.options) == code } + if let layoutOptions = layoutOptions { + return """ + var scaleOptions: ScaleTransformViewOptions { + .layout(.\(layoutOptions.layout.rawValue)) + } + """ + } + return code + + } else if let options = options as? StackTransformViewOptions { + + let code = generateCode(options: options) + let layoutOptions = StackTransformViewOptions.Layout.allCases + .map { (options: StackTransformViewOptions.layout($0), layout: $0) } + .first { generateCode(options: $0.options) == code } + if let layoutOptions = layoutOptions { + return """ + var stackOptions: StackTransformViewOptions { + .layout(.\(layoutOptions.layout.rawValue)) + } + """ + } + return code + + } else if let options = options as? SnapshotTransformViewOptions { + + let code = generateCode(options: options) + let layoutOptions = SnapshotTransformViewOptions.Layout.allCases + .map { (options: SnapshotTransformViewOptions.layout($0), layout: $0) } + .first { generateCode(options: $0.options) == code } + if let layoutOptions = layoutOptions { + return """ + var snapshotOptions: SnapshotTransformViewOptions { + .layout(.\(layoutOptions.layout.rawValue)) + } + """ + } + return code + } + return "" + } + + // MARK: Private functions + + private func generateCode(options: ScaleTransformViewOptions) -> String { + """ + var scaleOptions = ScaleTransformViewOptions( + minScale: \(options.minScale.format()), + maxScale: \(options.maxScale.format()), + scaleRatio: \(options.scaleRatio.format()), + translationRatio: \(options.translationRatio.generateInitCode()), + minTranslationRatio: \(options.minTranslationRatio.map { $0.generateInitCode() } ?? "nil"), + maxTranslationRatio: \(options.maxTranslationRatio.map { $0.generateInitCode() } ?? "nil"), + keepVerticalSpacingEqual: \(options.keepVerticalSpacingEqual ? "true" : "false"), + keepHorizontalSpacingEqual: \(options.keepHorizontalSpacingEqual ? "true" : "false"), + scaleCurve: \(options.scaleCurve.generateInitCode()), + translationCurve: \(options.translationCurve.generateInitCode()), + shadowEnabled: \(options.shadowEnabled ? "true" : "false"), + shadowColor: \(options.shadowColor.generateInitCode()), + shadowOpacity: \(CGFloat(options.shadowOpacity).format()), + shadowRadiusMin: \(options.shadowRadiusMin.format()), + shadowRadiusMax: \(options.shadowRadiusMax.format()), + shadowOffsetMin: \(options.shadowOffsetMin.generateInitCode()), + shadowOffsetMax: \(options.shadowOffsetMax.generateInitCode()), + shadowOpacityMin: \(CGFloat(options.shadowOpacityMin).format()), + shadowOpacityMax: \(CGFloat(options.shadowOpacityMax).format()), + blurEffectEnabled: \(options.blurEffectEnabled ? "true" : "false"), + blurEffectRadiusRatio: \(options.blurEffectRadiusRatio.format()), + blurEffectStyle: \(options.blurEffectStyle.generateInitCode()), + rotation3d: \(options.rotation3d.map { $0.generateInitCode() } ?? "nil"), + translation3d: \(options.translation3d.map { $0.generateInitCode() } ?? "nil") + ) + """ + } + + private func generateCode(options: StackTransformViewOptions) -> String { + """ + var stackOptions = StackTransformViewOptions( + scaleFactor: \(options.scaleFactor.format()), + minScale: \(options.minScale.map { $0.format() } ?? "nil"), + maxScale: \(options.maxScale.map { $0.format() } ?? "nil"), + maxStackSize: \(options.maxStackSize), + spacingFactor: \(options.spacingFactor.format()), + maxSpacing: \(options.maxSpacing.map { $0.format() } ?? "nil"), + alphaFactor: \(options.alphaFactor.format()), + bottomStackAlphaSpeedFactor: \(options.bottomStackAlphaSpeedFactor.format()), + topStackAlphaSpeedFactor: \(options.topStackAlphaSpeedFactor.format()), + perspectiveRatio: \(options.perspectiveRatio.format()), + shadowEnabled: \(options.shadowEnabled ? "true" : "false"), + shadowColor: \(options.shadowColor.generateInitCode()), + shadowOpacity: \(CGFloat(options.shadowOpacity).format()), + shadowOffset: \(options.shadowOffset.generateInitCode()), + shadowRadius: \(options.shadowRadius.format()), + stackRotateAngel: \(options.stackRotateAngel.format()), + popAngle: \(options.popAngle.format()), + popOffsetRatio: \(options.popOffsetRatio.generateInitCode()), + stackPosition: \(options.stackPosition.generateInitCode()), + reverse: \(options.reverse ? "true" : "false"), + blurEffectEnabled: \(options.blurEffectEnabled ? "true" : "false"), + maxBlurEffectRadius: \(options.maxBlurEffectRadius.format()), + blurEffectStyle: \(options.blurEffectStyle.generateInitCode()) + ) + """ + } + + private func generateCode(options: SnapshotTransformViewOptions) -> String { + """ + var snapshotOptions = SnapshotTransformViewOptions( + pieceSizeRatio: \(options.pieceSizeRatio.generateInitCode()), + piecesCornerRadiusRatio: \(options.piecesCornerRadiusRatio.generateInitCode()), + piecesAlphaRatio: \(options.piecesAlphaRatio.generateInitCode()), + piecesTranslationRatio: \(options.piecesTranslationRatio.generateInitCode()), + piecesScaleRatio: \(options.piecesScaleRatio.generateInitCode()), + containerScaleRatio: \(options.containerScaleRatio.format()), + containerTranslationRatio: \(options.containerTranslationRatio.generateInitCode()) + ) + """ + } +} + + +private extension SnapshotTransformViewOptions.PiecesValue { + func generateInitCode() -> String { + switch self { + case let .static(value): + return ".static(\(formatValue(value)))" + case let .columnBased(value, reversed): + return """ + .columnBased( + \(formatValue(value)), + reversed: \(reversed ? "true" : "false") + ) + """ + case let .rowBased(value, reversed): + return """ + .rowBased( + \(formatValue(value)), + reversed: \(reversed ? "true" : "false") + ) + """ + case let .columnOddEven(odd, even, increasing): + return """ + .columnOddEven( + \(formatValue(odd)), + \(formatValue(even)), + increasing: \(increasing ? "true" : "false") + ) + """ + case let .rowOddEven(odd, even, increasing): + return """ + .rowOddEven( + \(formatValue(odd)), + \(formatValue(even)), + increasing: \(increasing ? "true" : "false") + ) + """ + case let .columnBasedMirror(value, reversed): + return """ + .columnBasedMirror( + \(formatValue(value)), + reversed: \(reversed ? "true" : "false") + ) + """ + case let .rowBasedMirror(value, reversed): + return """ + .rowBasedMirror( + \(formatValue(value)), + reversed: \(reversed ? "true" : "false") + ) + """ + case let .indexBasedCustom(values): + return ".indexBasedCustom(\(values.map { formatValue($0) }.joined(separator: ", ")))" + case let .rowBasedCustom(values): + return ".rowBasedCustom(\(values.map { formatValue($0) }.joined(separator: ", ")))" + case let .columnBasedCustom(values): + return ".columnBasedCustom(\(values.map { formatValue($0) }.joined(separator: ", ")))" + case let .aggregated(piecesValue, _): + // Custom detection for the functions, I don't think there is a better way for that + // This works only on the current options + var function = "+" + if case .rowBasedMirror(let value, _) = piecesValue.first, + let point = value as? CGPoint, + point.x == 1 { + function = "*" + } + return ".aggregated([\(piecesValue.map { $0.generateInitCode() }.joined(separator: ", "))], \(function))" + } + } + + private func formatValue(_ value: Any) -> String { + if let value = value as? CGFloat { + return value.format() + } + if let value = value as? CGSize { + return value.generateInitCode() + } + if let value = value as? CGPoint { + return value.generateInitCode() + } + return "-" + } +} + + +private extension UIColor { + func generateInitCode() -> String { + if self == .black { + return ".black" + } + if self == .white { + return ".white" + } + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + return ".init(red: \(red.format()), green: \(green.format()), blue: \(blue.format()), alpha: \(alpha.format()))" + } +} + + +private extension ScaleTransformViewOptions.Translation3dOptions { + func generateInitCode() -> String { + """ + .init( + translateRatios: (\(translateRatios.0.format()), \(translateRatios.1.format()), \(translateRatios.2.format())), + minTranslateRatios: (\(minTranslateRatios.0.format()), \(minTranslateRatios.1.format()), \(minTranslateRatios.2.format())), + maxTranslateRatios: (\(maxTranslateRatios.0.format()), \(maxTranslateRatios.1.format()), \(maxTranslateRatios.2.format())) + ) + """ + } +} + + +private extension ScaleTransformViewOptions.Rotation3dOptions { + func generateInitCode() -> String { + """ + .init( + angle: \(angle.format()), + minAngle: \(minAngle.format()), + maxAngle: \(maxAngle.format()), + x: \(x.format()), + y: \(y.format()), + z: \(z.format()), + m34: \(m34.format(fractionDigits: 6)) + ) + """ + } +} + + +private extension CGSize { + func generateInitCode() -> String { + if self == .zero { return ".zero" } + return ".init(width: \(width.format()), height: \(height.format()))" + } +} + + +private extension CGPoint { + func generateInitCode() -> String { + if self == .zero { return ".zero" } + return ".init(x: \(x.format()), y: \(y.format()))" + } +} + + +private extension TransformCurve { + func generateInitCode() -> String { + ".\(name.prefix(1).lowercased() + name.dropFirst())" + } +} + +private extension UIBlurEffect.Style { + func generateInitCode() -> String { + ".\(name.prefix(1).lowercased() + name.dropFirst())" + } +} diff --git a/Samples/PagingLayoutSamples/Modules/Main/MainViewController.swift b/Samples/PagingLayoutSamples/Modules/Main/MainViewController.swift index 4b21886..b881919 100644 --- a/Samples/PagingLayoutSamples/Modules/Main/MainViewController.swift +++ b/Samples/PagingLayoutSamples/Modules/Main/MainViewController.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import SwiftUI class MainViewController: UIViewController, NibBased { @@ -16,75 +17,153 @@ class MainViewController: UIViewController, NibBased { override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } - + + @IBOutlet fileprivate weak var frameworkSegmentedControl: UISegmentedControl! + @IBOutlet private weak var transformTitleLabel: UILabel! + @IBOutlet private var transformSubtitles: [UILabel]! + @IBOutlet private weak var swiftUICustomBuildsContainer: UIView! + @IBOutlet private weak var uiKitCustomBuildsContainer: UIView! + + private var isSwiftUI: Bool { + frameworkSegmentedControl.selectedSegmentIndex == 0 + } + + + // MARK: ViewController + + override func viewDidLoad() { + super.viewDidLoad() + configureFrameworkSegmentedControl() + updateViewsBasedOnSelectedFramework() + swiftUICustomBuildsContainer.setNeedsLayout() + } + // MARK: Event listeners @IBAction private func stackButtonTouched() { - navigationController?.pushViewController( - ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: [ - .stackVortex, - .stackRotary, - .stackTransparent, - .stackBlur, - .stackReverse, - .stackPerspective - ])), - animated: true - ) + if isSwiftUI { + push(makeViewController(ShapesListView(layoutGroup: .stack), + backButtonColor: .gray, + statusBarStyle: .darkContent)) + } else { + push(ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: .stack))) + } } @IBAction private func scaleButtonTouched() { - navigationController?.pushViewController( - ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: [ - .scaleCylinder, - .scaleInvertedCylinder, - .scaleCoverFlow, - .scaleLinear, - .scaleEaseIn, - .scaleEaseOut, - .scaleRotary, - .scaleBlur - ])), - animated: true - ) + if isSwiftUI { + push(makeViewController(ShapesListView(layoutGroup: .scale), + backButtonColor: .gray, + statusBarStyle: .darkContent)) + } else { + push(ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: .scale))) + } } @IBAction private func snapshotButtonTouched() { - navigationController?.pushViewController( - ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: [ - .snapshotBars, - .snapshotFade, - .snapshotGrid, - .snapshotChess, - .snapshotLines, - .snapshotSpace, - .snapshotTiles, - .snapshotPuzzle - ])), - animated: true - ) + if isSwiftUI { + push(makeViewController(ShapesListView(layoutGroup: .snapshot), + backButtonColor: .gray, + statusBarStyle: .darkContent)) + } else { + push(ShapesViewController.instantiate(viewModel: ShapesViewModel(layouts: .snapshot))) + } } @IBAction private func fruitsButtonTouched() { - navigationController?.pushViewController( - FruitsViewController.instantiate(viewModel: FruitsViewModel()), - animated: true - ) + push(FruitsViewController.instantiate(viewModel: FruitsViewModel())) } @IBAction private func galleryButtonTouched() { - navigationController?.pushViewController( - GalleryViewController.instantiate(viewModel: GalleryViewModel()), - animated: true - ) + push(GalleryViewController.instantiate(viewModel: GalleryViewModel())) } @IBAction private func cardsButtonTouched() { - navigationController?.pushViewController( - CardsViewController.instantiate(viewModel: CardsViewModel()), - animated: true - ) + push(CardsViewController.instantiate(viewModel: CardsViewModel())) + } + + @IBAction private func devicesButtonTouched() { + push(makeViewController(DevicesView(), backButtonColor: .white, statusBarStyle: .lightContent)) + } + + @IBAction private func transportButtonTouched() { + // TODO: + } + + @IBAction private func weatherButtonTouched() { + push(makeViewController(WeatherTabView(), backButtonColor: .gray, statusBarStyle: .darkContent)) + } + + @IBAction private func onFrameworkChanged() { + updateViewsBasedOnSelectedFramework() + } + + + // MARK: Private functions + + private func configureFrameworkSegmentedControl() { + frameworkSegmentedControl.overrideUserInterfaceStyle = .dark + } + + private func updateViewsBasedOnSelectedFramework() { + transformTitleLabel.text = isSwiftUI ? "PageView" : "Transforms" + transformSubtitles.forEach { $0.text = transformTitleLabel.text } + + swiftUICustomBuildsContainer.isHidden = !isSwiftUI + uiKitCustomBuildsContainer.isHidden = isSwiftUI + } + + private func push(_ viewController: UIViewController) { + navigationController?.pushViewController(viewController, animated: true) + } + + @objc private func pop() { + navigationController?.popViewController(animated: true) + } + + private func makeViewController(_ view: T, + backButtonColor: UIColor?, + statusBarStyle: UIStatusBarStyle?) -> UIViewController { + let viewController = MainHostingViewController() + viewController.statusBarStyle = statusBarStyle + viewController.view.fill(with: UIHostingController(rootView: view).view) + if let backButtonColor = backButtonColor { + let backButton = UIButton(type: .custom) + backButton.setImage(UIImage(systemName: "arrow.left.square.fill", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .semibold)), + for: .normal) + backButton.translatesAutoresizingMaskIntoConstraints = false + viewController.view.addSubview(backButton) + backButton.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor, constant: 32).isActive = true + backButton.topAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.topAnchor, constant: 19).isActive = true + backButton.tintColor = backButtonColor + backButton.addTarget(self, action: #selector(pop), for: .touchUpInside) + } + + return viewController + } +} + + +class MainHostingViewController: UIViewController { + + fileprivate var statusBarStyle: UIStatusBarStyle? + + override var preferredStatusBarStyle: UIStatusBarStyle { + statusBarStyle ?? .lightContent + } +} + + +class MainViewControllerScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let mainVC = superview?.next as? MainViewController { + let point = convert(point, to: mainVC.view) + if mainVC.frameworkSegmentedControl.frame.contains(point) { + return mainVC.frameworkSegmentedControl.hitTest(point, with: event) + } + } + return super.hitTest(point, with: event) } - } diff --git a/Samples/PagingLayoutSamples/Modules/Main/MainViewController.xib b/Samples/PagingLayoutSamples/Modules/Main/MainViewController.xib index 3292e12..cbe3a62 100644 --- a/Samples/PagingLayoutSamples/Modules/Main/MainViewController.xib +++ b/Samples/PagingLayoutSamples/Modules/Main/MainViewController.xib @@ -1,15 +1,24 @@ - - + + - + + + - + + + + + + + + @@ -17,30 +26,53 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -67,366 +99,543 @@ - - + + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + - + - - - - - - + - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + - - - + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + - + - - - - - - + - - - - - - - - - + + - + + - + + + - - - - + + - - + - + + @@ -434,5 +643,9 @@ + + + + diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/ScaleShapeCollectionViewCells.swift b/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/ScaleShapeCollectionViewCells.swift deleted file mode 100644 index 047d31b..0000000 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/ScaleShapeCollectionViewCells.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// ScaleShapeCollectionViewCell.swift -// CollectionViewPagingLayout -// -// Created by Amir on 15/02/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. -// - -import UIKit -import CollectionViewPagingLayout - -class ScaleLinearShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.6, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.66, y: 0.2), - maxTranslationRatio: CGPoint(x: 2, y: 0), - keepVerticalSpacingEqual: true, - keepHorizontalSpacingEqual: true, - scaleCurve: .linear, - translationCurve: .linear - ) -} - - -class ScaleEaseInShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.6, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.66, y: 0.2), - keepVerticalSpacingEqual: true, - keepHorizontalSpacingEqual: true, - scaleCurve: .easeIn, - translationCurve: .linear - ) -} - - -class ScaleEaseOutShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.6, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.66, y: 0.2), - keepVerticalSpacingEqual: true, - keepHorizontalSpacingEqual: true, - scaleCurve: .linear, - translationCurve: .easeIn - ) -} - - -class ScaleCylinderShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.55, - maxScale: 0.55, - scaleRatio: 0, - translationRatio: .zero, - minTranslationRatio: .zero, - maxTranslationRatio: .zero, - shadowEnabled: false, - rotation3d: .init(angle: .pi / 4, minAngle: -.pi, maxAngle: .pi, x: 0, y: 1, z: 0, m34: -0.001_2), - translation3d: .init(translateRatios: (0, 0, 0), minTranslates: (0, 0, UIScreen.main.bounds.width * 0.8), maxTranslates: (0, 0, UIScreen.main.bounds.width * 0.8)) - ) -} - - -class ScaleInvertedCylinderShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 1.2, - maxScale: 1.2, - scaleRatio: 0, - translationRatio: .zero, - minTranslationRatio: .zero, - maxTranslationRatio: .zero, - shadowEnabled: false, - rotation3d: .init(angle: .pi / 3, minAngle: -.pi, maxAngle: .pi, x: 0, y: -1, z: 0, m34: -0.002), - translation3d: .init(translateRatios: (0, 0, 0), minTranslates: (0, 0, UIScreen.main.bounds.width * 0.57), maxTranslates: (0, 0, -UIScreen.main.bounds.width * 0.57)) - ) -} - - -class ScaleCoverFlowShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.7, - maxScale: 0.7, - scaleRatio: 0, - translationRatio: .zero, - 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: (30, 0, -UIScreen.main.bounds.width * 0.42), minTranslates: (-30, 0, -1_000), maxTranslates: (30, 0, 0)) - ) -} - - -class ScaleBlurShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0.6, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.66, y: 0.2), - maxTranslationRatio: CGPoint(x: 2, y: 0), - blurEffectEnabled: true, - blurEffectRadiusRatio: 0.2 - ) -} - - -class ScaleRotaryShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { - - var scaleOptions = ScaleTransformViewOptions( - minScale: 0, - scaleRatio: 0.4, - translationRatio: CGPoint(x: 0.1, y: 0.1), - minTranslationRatio: CGPoint(x: -1, y: 0), - maxTranslationRatio: CGPoint(x: 1, y: 1), - rotation3d: .init(angle: .pi / 15, minAngle: -.pi / 3, maxAngle: .pi / 3, x: 0, y: 0, z: 1, m34: -0.004), - translation3d: .init(translateRatios: (200, UIScreen.main.bounds.width * 0.1, 0), - minTranslates: (-1_000, -UIScreen.main.bounds.width, -100), - maxTranslates: (1_000, UIScreen.main.bounds.width, -100)) - ) -} diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/SnapshotShapeCollectionViewCells.swift b/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/SnapshotShapeCollectionViewCells.swift deleted file mode 100644 index 3e003dd..0000000 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/SnapshotShapeCollectionViewCells.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// SnapshotShapeCollectionViewCells.swift -// CollectionViewPagingLayout -// -// Created by Amir on 07/03/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. -// - -import UIKit -import CollectionViewPagingLayout - -class GridSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1.0 / 4.0, height: 1.0 / 8.0), - piecesCornerRadiusRatio: .static(1), - piecesAlphaRatio: .static(0), - piecesTranslationRatio: .aggregated([.rowBasedMirror(CGPoint(x: 0, y: -1.8)), .columnBasedMirror(CGPoint(x: -1.8, y: 0))], +), - piecesScaleRatio: .static(.init(width: 0.8, height: 0.8)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 0.7, y: 0) - ) -} - - -class SpaceSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1.0 / 3.0, height: 1.0 / 4.0), - piecesCornerRadiusRatio: .static(0.7), - piecesAlphaRatio: .aggregated([.rowBasedMirror(0.2), .columnBasedMirror(0.4)], +), - piecesTranslationRatio: .aggregated([.rowBasedMirror(CGPoint(x: 1, y: -1)), .columnBasedMirror(CGPoint(x: -1, y: 1))], *), - piecesScaleRatio: .static(.init(width: 0.5, height: 0.5)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 0.7, y: 0) - ) -} - - -class ChessSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1.0 / 5.0, height: 1.0 / 8.0), - piecesCornerRadiusRatio: .static(0.5), - piecesAlphaRatio: .columnBasedMirror(0.4), - piecesTranslationRatio: .columnBasedMirror(CGPoint(x: -1, y: 1)), - piecesScaleRatio: .static(.init(width: 0.5, height: 0.5)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 0.7, y: 0) - ) -} - - -class TilesSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1, height: 1.0 / 8.0), - piecesCornerRadiusRatio: .static(0), - piecesAlphaRatio: .static(0.4), - piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.4, y: 0), CGPoint(x: 0.4, y: 0)), - piecesScaleRatio: .static(.init(width: 0, height: 0.1)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 1, y: 0) - ) -} - - -class LinesSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1, height: 1.0 / 16.0), - piecesCornerRadiusRatio: .static(0), - piecesAlphaRatio: .static(0.4), - piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.15, y: 0), CGPoint(x: 0.15, y: 0)), - piecesScaleRatio: .static(.init(width: 0.6, height: 0.96)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 0.8, y: 0) - ) -} - - -class BarsSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1.0 / 10.0, height: 1), - piecesCornerRadiusRatio: .static(1.2), - piecesAlphaRatio: .static(0.4), - piecesTranslationRatio: .columnOddEven(CGPoint(x: 0, y: -0.1), CGPoint(x: 0, y: 0.1)), - piecesScaleRatio: .static(.init(width: 0.2, height: 0.6)), - containerScaleRatio: 0.1, - containerTranslationRatio: .init(x: 1, y: 0) - ) -} - - -class PuzzleSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1.0 / 4.0, height: 1.0 / 8.0), - piecesCornerRadiusRatio: .static(0), - piecesAlphaRatio: .aggregated([.rowOddEven(0.2, 0), .columnOddEven(0, 0.2)], +), - piecesTranslationRatio: .rowOddEven(CGPoint(x: -0.15, y: 0), CGPoint(x: 0.15, y: 0)), - piecesScaleRatio: .columnOddEven(.init(width: 0.1, height: 0.4), .init(width: 0.4, height: 0.1)), - containerScaleRatio: 0.2, - containerTranslationRatio: .init(x: 1, y: 0) - ) -} - - -class FadeSnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { - var snapshotOptions: SnapshotTransformViewOptions = .init( - pieceSizeRatio: .init(width: 1, height: 1.0 / 12.0), - piecesCornerRadiusRatio: .static(0.1), - piecesAlphaRatio: .rowBased(0.1), - piecesTranslationRatio: .rowBasedMirror(CGPoint(x: 0, y: 0.1)), - piecesScaleRatio: .rowBasedMirror(.init(width: 0.05, height: 0.1)), - containerScaleRatio: 0.7, - containerTranslationRatio: .init(x: 1.9, y: 0) - ) -} diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/StackShapeCollectionViewCells.swift b/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/StackShapeCollectionViewCells.swift deleted file mode 100644 index fc806db..0000000 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/StackShapeCollectionViewCells.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// StackShapeCollectionViewCells.swift -// CollectionViewPagingLayout -// -// Created by Amir on 21/02/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. -// - -import UIKit -import CollectionViewPagingLayout - -class TransparentStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: 0.12, - minScale: 0.0, - maxStackSize: 4, - alphaFactor: 0.2, - bottomStackAlphaSpeedFactor: 10, - topStackAlphaSpeedFactor: 0.1, - popAngle: .pi / 10, - popOffsetRatio: .init(width: -1.45, height: 0.3) - ) -} - - -class PerspectiveStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: 0.1, - minScale: 0.2, - maxStackSize: 6, - spacingFactor: 0.08, - alphaFactor: 0.0, - perspectiveRatio: 0.3, - shadowRadius: 5, - popAngle: .pi / 10, - popOffsetRatio: .init(width: -1.45, height: 0.3), - stackPosition: CGPoint(x: 1, y: 0) - ) -} - - -class RotaryStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: -0.03, - minScale: 0.2, - maxStackSize: 3, - spacingFactor: 0.01, - alphaFactor: 0.1, - shadowRadius: 8, - stackRotateAngel: .pi / 16, - popAngle: .pi / 4, - popOffsetRatio: .init(width: -1.45, height: 0.4), - stackPosition: CGPoint(x: 0, y: 1) - ) -} - - -class VortexStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: -0.15, - minScale: 0.2, - maxScale: nil, - maxStackSize: 4, - spacingFactor: 0, - alphaFactor: 0.4, - topStackAlphaSpeedFactor: 1, - perspectiveRatio: -0.3, - shadowEnabled: false, - popAngle: .pi, - popOffsetRatio: .zero, - stackPosition: CGPoint(x: 0, y: 1) - ) -} - - -class ReverseStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: 0.1, - maxScale: nil, - maxStackSize: 4, - spacingFactor: 0.08, - shadowRadius: 8, - popAngle: -.pi / 4, - popOffsetRatio: .init(width: 1.45, height: 0.4), - stackPosition: CGPoint(x: -1, y: -0.2), - reverse: true - ) -} - - -class BlurStackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { - - var stackOptions: StackTransformViewOptions = .init( - scaleFactor: 0.1, - maxScale: nil, - maxStackSize: 7, - spacingFactor: 0.06, - topStackAlphaSpeedFactor: 0.1, - perspectiveRatio: 0.04, - shadowRadius: 8, - popAngle: -.pi / 4, - popOffsetRatio: .init(width: 1.45, height: 0.4), - stackPosition: CGPoint(x: -1, y: 0), - reverse: true, - blurEffectEnabled: true, - maxBlurEffectRadius: 0.08 - ) -} diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift b/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift deleted file mode 100644 index 87accd3..0000000 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.swift +++ /dev/null @@ -1,160 +0,0 @@ -// -// ShapesViewController.swift -// CollectionViewPagingLayout -// -// Created by Amir on 15/02/2020. -// Copyright © 2020 Amir Khorsandi. All rights reserved. -// - -import UIKit -import CollectionViewPagingLayout - -class ShapesViewController: UIViewController, NibBased, ViewModelBased { - - // MARK: Constants - - private struct Constants { - - static let infiniteNumberOfItems = 100_000 - } - - - // MARK: Properties - - var viewModel: ShapesViewModel! - - @IBOutlet private weak var collectionView: UICollectionView! - @IBOutlet private weak var layoutTypeCollectionView: UICollectionView! - - private var didScrollCollectionViewToMiddle = false - - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - configureViews() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if !didScrollCollectionViewToMiddle { - let layout = layoutTypeCollectionView.collectionViewLayout as? CollectionViewPagingLayout - layout?.setCurrentPage(Constants.infiniteNumberOfItems / 2, animated: false) - didScrollCollectionViewToMiddle = true - } - - layoutTypeCollectionView.collectionViewLayout.invalidateLayout() - updateSelectedLayout() - } - - // MARK: Event listener - - @IBAction private func onBackTouched() { - navigationController?.popViewController(animated: true) - } - - - // MARK: Private functions - - private func configureViews() { - view.backgroundColor = #colorLiteral(red: 0.9529411765, green: 0.9529411765, blue: 0.9529411765, alpha: 1) - view.clipsToBounds = true - configureCollectionView() - configureLayoutTypeCollectionView() - } - - private func configureCollectionView() { - ShapesViewModel.allLayoutViewModes.forEach { - collectionView.registerClass($0.cellClass, reuseIdentifier: "\($0.layout)") - } - - collectionView.isPagingEnabled = true - collectionView.dataSource = self - let layout = CollectionViewPagingLayout() - layout.numberOfVisibleItems = 10 - collectionView.collectionViewLayout = layout - collectionView.showsHorizontalScrollIndicator = false - collectionView.clipsToBounds = false - collectionView.backgroundColor = .clear - } - - private func configureLayoutTypeCollectionView() { - layoutTypeCollectionView.register(LayoutTypeCollectionViewCell.self) - layoutTypeCollectionView.isPagingEnabled = true - layoutTypeCollectionView.dataSource = self - let layout = CollectionViewPagingLayout() - layout.numberOfVisibleItems = 5 - layoutTypeCollectionView.collectionViewLayout = layout - layoutTypeCollectionView.showsHorizontalScrollIndicator = false - layoutTypeCollectionView.clipsToBounds = false - layoutTypeCollectionView.backgroundColor = .clear - layoutTypeCollectionView.delegate = self - } - - private func updateSelectedLayout() { - guard let layout = layoutTypeCollectionView.collectionViewLayout as? CollectionViewPagingLayout else { - return - } - let index = layout.currentPage % viewModel.layoutTypeViewModels.count - self.viewModel.selectedLayout = self.viewModel.layoutTypeViewModels[index] - collectionView.reloadData() - collectionView.performBatchUpdates({ - self.collectionView.collectionViewLayout.invalidateLayout() - }, completion: nil) - } -} - -extension ShapesViewController: UICollectionViewDataSource { - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if collectionView == layoutTypeCollectionView { - return Constants.infiniteNumberOfItems - } - if collectionView == self.collectionView { - return viewModel.selectedLayout.cardViewModels.count - } - - fatalError("unknown collection view") - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - if collectionView == layoutTypeCollectionView { - let itemViewModel = viewModel.layoutTypeViewModels[indexPath.row % viewModel.layoutTypeViewModels.count] - let cell: LayoutTypeCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath) - cell.viewModel = itemViewModel - return cell - } - - if collectionView == self.collectionView { - let itemViewModel = viewModel.selectedLayout.cardViewModels[indexPath.row] - let cell = collectionView.dequeueReusableCellClass(for: indexPath, type: viewModel.selectedLayout.cellClass, reuseIdentifier: "\(viewModel.selectedLayout.layout)") - cell.viewModel = itemViewModel - return cell - } - - fatalError("unknown collection view") - } - -} - -extension ShapesViewController: UICollectionViewDelegate { - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - guard scrollView == layoutTypeCollectionView else { - return - } - if !decelerate { - updateSelectedLayout() - } - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - guard scrollView == layoutTypeCollectionView else { - return - } - updateSelectedLayout() - } - -} 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 97% rename from Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.swift index cfc84a8..7a74885 100644 --- a/Samples/PagingLayoutSamples/Modules/Fruits/Cell/FruitsCollectionViewCell.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Fruits/Cell/FruitsCollectionViewCell.swift @@ -157,9 +157,11 @@ extension FruitsCollectionViewCell: QuantityControllerViewDelegate { // this is an example, in an ideal world we want to save the new quantity properly func onDecreaseButtonTouched(view: QuantityControllerView) { - viewModel?.quantity = max(0, (viewModel?.quantity ?? 0) - 1) + let current = viewModel?.quantity ?? 0 + viewModel?.quantity = max(0, current - 1) } func onIncreaseButtonTouched(view: QuantityControllerView) { - viewModel?.quantity = (viewModel?.quantity ?? 0) + 1 + let current = viewModel?.quantity ?? 0 + viewModel?.quantity = current + 1 } } 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 88% rename from Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift index 8f2f2a7..03aa70b 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCellViewModel.swift @@ -15,5 +15,4 @@ struct LayoutTypeCellViewModel { let title: String let subtitle: String let cardViewModels: [ShapeCardViewModel] - let cellClass: BaseShapeCollectionViewCell.Type } diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift similarity index 96% rename from Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift index 7594a3e..11991d3 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/LayoutTypeCell/LayoutTypeCollectionViewCell.swift @@ -61,6 +61,10 @@ extension LayoutTypeCollectionViewCell: ScaleTransformView { circleView } + var selectableView: UIView? { + circleView + } + func transform(progress: CGFloat) { applyScaleTransform(progress: progress) titleLabel.alpha = 1 - abs(progress) 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 52% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift index a17af59..451ac1e 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/BaseShapeCollectionViewCell.swift @@ -19,6 +19,7 @@ class BaseShapeCollectionViewCell: UICollectionViewCell { } private(set) var shapeCardView: ShapeCardView! + private var edgeConstraints: [NSLayoutConstraint]? // MARK: Lifecycle @@ -34,14 +35,31 @@ class BaseShapeCollectionViewCell: UICollectionViewCell { } + override func layoutSubviews() { + super.layoutSubviews() + guard let edgeConstraints = edgeConstraints else { + return + } + let leftRightMargin = frame.width * 0.18 + let topBottomMargin = frame.height * 0.06 + edgeConstraints[0].constant = leftRightMargin + edgeConstraints[2].constant = -leftRightMargin + + edgeConstraints[1].constant = topBottomMargin + edgeConstraints[3].constant = -topBottomMargin + } + + // MARK: Private functions private func setupViews() { shapeCardView = ShapeCardView.instantiate() - let leftRightMargin = UIScreen.main.bounds.width * 0.18 - let topBottomMargin = UIScreen.main.bounds.height * 0.06 - contentView.fill(with: shapeCardView, - edges: UIEdgeInsets(top: topBottomMargin, left: leftRightMargin, bottom: -topBottomMargin, right: -leftRightMargin)) + let leftRightMargin = frame.width * 0.18 + let topBottomMargin = frame.height * 0.06 + edgeConstraints = contentView.fill( + with: shapeCardView, + edges: UIEdgeInsets(top: topBottomMargin, left: leftRightMargin, bottom: -topBottomMargin, right: -leftRightMargin) + ) clipsToBounds = false contentView.clipsToBounds = false } diff --git a/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/ShapeCollectionViewCells.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/ShapeCollectionViewCells.swift new file mode 100644 index 0000000..a366545 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapeCell/ShapeCollectionViewCells.swift @@ -0,0 +1,30 @@ +// +// ShapeCollectionViewCells.swift +// CollectionViewPagingLayout +// +// Created by Amir on 15/02/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit +import CollectionViewPagingLayout + +class ScaleShapeCollectionViewCell: BaseShapeCollectionViewCell, ScaleTransformView { + var scaleOptions: ScaleTransformViewOptions { + ShapesViewModel.scaleOptions + } +} + + +class StackShapeCollectionViewCell: BaseShapeCollectionViewCell, StackTransformView { + var stackOptions: StackTransformViewOptions { + ShapesViewModel.stackOptions + } +} + + +class SnapshotShapeCollectionViewCell: BaseShapeCollectionViewCell, SnapshotTransformView { + var snapshotOptions: SnapshotTransformViewOptions { + ShapesViewModel.snapshotOptions + } +} 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/UIKit/Shapes/ShapesViewController.swift b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.swift new file mode 100644 index 0000000..8a07842 --- /dev/null +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.swift @@ -0,0 +1,259 @@ +// +// ShapesViewController.swift +// CollectionViewPagingLayout +// +// Created by Amir on 15/02/2020. +// Copyright © 2020 Amir Khorsandi. All rights reserved. +// + +import UIKit +import CollectionViewPagingLayout + +protocol ShapesViewControllerDelegate: AnyObject { + func shapesViewController(_ vc: ShapesViewController, onSelectedLayoutChange layout: ShapeLayout) +} + + +class ShapesViewController: UIViewController, NibBased, ViewModelBased { + + // MARK: Constants + + private struct Constants { + + static let infiniteNumberOfItems = 10_000 + } + + + // MARK: Properties + + var viewModel: ShapesViewModel! { + didSet { + updateSelectedLayout() + reloadDataAndLayouts() + } + } + + weak var delegate: ShapesViewControllerDelegate? + + @IBOutlet private weak var backButton: UIButton! + @IBOutlet private weak var collectionView: UICollectionView! + @IBOutlet private weak var layoutTypeCollectionView: UICollectionView! + @IBOutlet private weak var pageControlView: PageControlView! + + private var didScrollCollectionViewToMiddle = false + + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + configureViews() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if !didScrollCollectionViewToMiddle { + didScrollCollectionViewToMiddle = true + collectionView?.performBatchUpdates({ [weak self] in + self?.getPagingLayout(layoutTypeCollectionView)?.setCurrentPage( + Constants.infiniteNumberOfItems / 2, + animated: false + ) + }) + } + + updateSelectedLayout() + getPagingLayout(layoutTypeCollectionView)?.invalidateLayoutInBatchUpdate() + } + + + // MARK: Public functions + + func reloadAndInvalidateShapes() { + collectionView?.reloadData() + invalidateCollectionViewLayout() + } + + + // MARK: Event listener + + @IBAction private func onBackTouched() { + navigationController?.popViewController(animated: true) + } + + @IBAction private func onNextButtonTouched() { + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.goToNextPage() + } + + @IBAction private func onPreviousButtonTouched() { + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.goToPreviousPage() + } + + + // MARK: Private functions + + private func configureViews() { + view.backgroundColor = #colorLiteral(red: 0.9529411765, green: 0.9529411765, blue: 0.9529411765, alpha: 1) + view.clipsToBounds = true + backButton.isHidden = !viewModel.showBackButton + configureCollectionView() + configureLayoutTypeCollectionView() + + pageControlView.numberOfPages = 8 + pageControlView.preferences = .init(color: UIColor.black.withAlphaComponent(0.5), dimFactor: 0.2, dotRadius: 5, gapSize: 6, currentDotBorderWidth: 3.5) + pageControlView.superview?.isHidden = !viewModel.showPageControl + } + + private func configureCollectionView() { + collectionView.registerClass(StackShapeCollectionViewCell.self) + collectionView.registerClass(ScaleShapeCollectionViewCell.self) + collectionView.registerClass(SnapshotShapeCollectionViewCell.self) + + collectionView.isPagingEnabled = true + collectionView.dataSource = self + let layout = CollectionViewPagingLayout() + collectionView.collectionViewLayout = layout + layout.delegate = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.clipsToBounds = false + collectionView.backgroundColor = .clear + collectionView.delegate = self + } + + private func configureLayoutTypeCollectionView() { + layoutTypeCollectionView.register(LayoutTypeCollectionViewCell.self) + layoutTypeCollectionView.isPagingEnabled = true + layoutTypeCollectionView.dataSource = self + let layout = CollectionViewPagingLayout() + layout.numberOfVisibleItems = 10 + layoutTypeCollectionView.collectionViewLayout = layout + layoutTypeCollectionView.showsHorizontalScrollIndicator = false + layoutTypeCollectionView.clipsToBounds = false + layoutTypeCollectionView.backgroundColor = .clear + layoutTypeCollectionView.delegate = self + } + + private func updateSelectedLayout() { + 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) + reloadAndInvalidateShapes() + } + + private func reloadDataAndLayouts() { + layoutTypeCollectionView?.reloadData() + collectionView?.reloadData() + invalidateLayouts() + } + + private func invalidateLayouts() { + invalidateLayoutTypeCollectionViewLayout() + invalidateCollectionViewLayout() + } + + private func invalidateLayoutTypeCollectionViewLayout() { + layoutTypeCollectionView?.performBatchUpdates({ [weak self] in + self?.layoutTypeCollectionView?.collectionViewLayout.invalidateLayout() + }) + } + + private func invalidateCollectionViewLayout() { + collectionView?.performBatchUpdates({ [weak self] in + self?.collectionView?.collectionViewLayout.invalidateLayout() + }) + } + + private func getPagingLayout(_ collectionView: UICollectionView?) -> CollectionViewPagingLayout? { + collectionView?.collectionViewLayout as? CollectionViewPagingLayout + } + +} + + +extension ShapesViewController: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + if collectionView == layoutTypeCollectionView { + return Constants.infiniteNumberOfItems + } + if collectionView == self.collectionView { + return viewModel.selectedLayout.cardViewModels.count + } + + fatalError("unknown collection view") + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + if collectionView == layoutTypeCollectionView { + let itemViewModel = viewModel.layoutTypeViewModels[indexPath.row % viewModel.layoutTypeViewModels.count] + let cell: LayoutTypeCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath) + cell.viewModel = itemViewModel + return cell + } + + if collectionView == self.collectionView { + let itemViewModel = viewModel.selectedLayout.cardViewModels[indexPath.row] + var cell: BaseShapeCollectionViewCell! + if ShapeLayout.scaleLayouts.contains(viewModel.selectedLayout.layout) { + cell = collectionView.dequeueReusableCellClass(for: indexPath) as ScaleShapeCollectionViewCell + + } else if ShapeLayout.stackLayouts.contains(viewModel.selectedLayout.layout) { + cell = collectionView.dequeueReusableCellClass(for: indexPath) as StackShapeCollectionViewCell + + } else if ShapeLayout.snapshotLayouts.contains(viewModel.selectedLayout.layout) { + cell = collectionView.dequeueReusableCellClass(for: indexPath) as SnapshotShapeCollectionViewCell + } + + cell.viewModel = itemViewModel + return cell + } + + fatalError("unknown collection view") + } + +} + + +extension ShapesViewController: UICollectionViewDelegate { + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + guard scrollView == layoutTypeCollectionView else { + return + } + if !decelerate { + updateSelectedLayout() + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard scrollView == layoutTypeCollectionView else { + return + } + updateSelectedLayout() + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + guard scrollView == layoutTypeCollectionView else { + return + } + updateSelectedLayout() + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + (collectionView.collectionViewLayout as? CollectionViewPagingLayout)?.setCurrentPage(indexPath.row) { [weak self] in + DispatchQueue.main.async { + self?.updateSelectedLayout() + } + } + } +} + + +extension ShapesViewController: CollectionViewPagingLayoutDelegate { + func onCurrentPageChanged(layout: CollectionViewPagingLayout, currentPage: Int) { + guard layout.collectionView == collectionView else { return } + pageControlView.currentPage = currentPage + } +} diff --git a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.xib b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.xib similarity index 59% rename from Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.xib rename to Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.xib index 05886ba..8367f48 100644 --- a/Samples/PagingLayoutSamples/Modules/Shapes/ShapesViewController.xib +++ b/Samples/PagingLayoutSamples/Modules/UIKit/Shapes/ShapesViewController.xib @@ -1,16 +1,18 @@ - + - + - + + + @@ -20,7 +22,7 @@ - + @@ -33,7 +35,7 @@ - + @@ -42,6 +44,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +