├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── swift.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── SharkStackKit.xcscheme ├── Assets ├── example-1.png ├── example-2.png ├── hstack-example.png ├── margin-example.png ├── sizing-example.png ├── spacing-example.png ├── stack-kit-logo.png ├── stackception-example.png ├── stackwithinastack.png └── vstack-example.png ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme └── Example │ ├── Flow │ ├── App Delegate │ │ └── AppDelegate.swift │ └── Feed │ │ ├── Cells │ │ ├── Feed Item │ │ │ ├── FeedItemCell.swift │ │ │ └── FeedItemViewModel.swift │ │ ├── Header │ │ │ ├── HeaderCell.swift │ │ │ └── HeaderCellViewModel.swift │ │ └── Order │ │ │ ├── OrderCell.swift │ │ │ └── OrderCellViewModel.swift │ │ └── Controller │ │ ├── FeedViewController.swift │ │ └── FeedViewModel.swift │ ├── Helpers │ └── CollectionExtension.swift │ ├── Models │ ├── FeedModel.swift │ ├── OrderModel.swift │ └── ProductModel.swift │ ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Assets │ │ │ ├── Contents.json │ │ │ ├── button-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── button-icon.pdf │ │ │ ├── more-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── more-icon.pdf │ │ │ └── search-icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── search-icon.pdf │ │ ├── Contents.json │ │ ├── Feed │ │ │ ├── Contents.json │ │ │ ├── flex.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── reset.jpg │ │ │ ├── functional-female.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── IMG_5650.jpg │ │ │ ├── functional-male.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── IMG_5651.jpg │ │ │ ├── must-have.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── IMG_5663.jpg │ │ │ ├── raise.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── IMG_5662.jpg │ │ │ └── reset.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── IMG_5661.jpg │ │ └── Products │ │ │ ├── Contents.json │ │ │ ├── hoodie.imageset │ │ │ ├── Contents.json │ │ │ └── hoodie.jpg │ │ │ ├── shorts.imageset │ │ │ ├── Contents.json │ │ │ └── shorts.jpg │ │ │ ├── top.imageset │ │ │ ├── Contents.json │ │ │ └── top.jpg │ │ │ └── tshirt.imageset │ │ │ ├── Contents.json │ │ │ └── tshirt.jpg │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Constants.swift │ └── Info.plist │ └── UI Components │ ├── Order Image Gallery │ ├── OrderGalleryImage.swift │ └── OrderGalleryImageView.swift │ └── Tag View │ └── TagView.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── StackKit │ ├── Stacks │ ├── Helpers │ │ ├── Spacer.swift │ │ └── UIViewHelpers.swift │ ├── Scroll │ │ ├── ScrollStacks.swift │ │ └── ScrollViewBuilder.swift │ └── Stacks │ │ ├── StackBuilder.swift │ │ └── Stacks.swift │ └── UIViewBuilder.swift ├── StackKit.podspec └── Tests ├── LinuxMain.swift └── StackKitTests ├── StackKitTests.swift └── XCTestManifests.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @global-owner1 and @global-owner2 will be requested for 7 | # review when someone opens a pull request. 8 | * @russshark @markdarling @TheiOSDude @Christopher1613 @vrutings @bolshedvorsky 9 | 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to StackKit 2 | 3 | - Write Code. 4 | - Merge Requests should go to MASTER. 5 | 6 | #### **Automation** 7 | 8 | On each Pull Request, Github Actions will; 9 | 10 | - Check it builds. 11 | - Check the unit tests run. 12 | 13 | Questions; Ask any of the Gymshark iOS Engineering Team. 14 | Twitter: @TheiOSDude, @vrutin, @russelltwarwick 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Current Behavior 7 | 8 | 9 | ## Possible Solution 10 | 11 | 12 | ## Steps to Reproduce 13 | 14 | 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | ## Context (Environment) 21 | 22 | 23 | 24 | 25 | 26 | ## Detailed Description 27 | 28 | 29 | ## Possible Implementation 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 6 | 7 | Fixes # (issue) 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | ## How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 21 | 22 | - [ ] Unit Tests 23 | 24 | ## Checklist: 25 | 26 | - [ ] My code follows the style guidelines of this project 27 | - [ ] I have performed a self-review of my own code 28 | - [ ] I have commented my code, particularly in hard-to-understand areas 29 | - [ ] I have made corresponding changes to the documentation 30 | - [ ] My changes generate no new warnings 31 | - [ ] I have added tests that prove my fix is effective or that my feature works 32 | - [ ] New and existing unit tests pass locally with my changes 33 | - [ ] Any dependent changes have been merged and published in downstream modules 34 | - [ ] I have checked my code and corrected any misspellings -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build for iOS 17 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild build-for-testing -scheme StackKit -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty 18 | - name: Run iOS tests 19 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -scheme StackKit -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SharkStackKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Assets/example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/example-1.png -------------------------------------------------------------------------------- /Assets/example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/example-2.png -------------------------------------------------------------------------------- /Assets/hstack-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/hstack-example.png -------------------------------------------------------------------------------- /Assets/margin-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/margin-example.png -------------------------------------------------------------------------------- /Assets/sizing-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/sizing-example.png -------------------------------------------------------------------------------- /Assets/spacing-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/spacing-example.png -------------------------------------------------------------------------------- /Assets/stack-kit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/stack-kit-logo.png -------------------------------------------------------------------------------- /Assets/stackception-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/stackception-example.png -------------------------------------------------------------------------------- /Assets/stackwithinastack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/stackwithinastack.png -------------------------------------------------------------------------------- /Assets/vstack-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Assets/vstack-example.png -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BC4F374726A7540100B41D71 /* StackKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC4F374626A7540100B41D71 /* StackKit */; }; 11 | BC5020E72645EAE500E7E234 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC5020E62645EAE500E7E234 /* Assets.xcassets */; }; 12 | BC5020EA2645EAE500E7E234 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BC5020E82645EAE500E7E234 /* LaunchScreen.storyboard */; }; 13 | BC6E946226A471AA00D91640 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6E946126A471AA00D91640 /* TagView.swift */; }; 14 | BC6E946426A5B41A00D91640 /* OrderGalleryImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6E946326A5B41A00D91640 /* OrderGalleryImageView.swift */; }; 15 | BC6E946726A5CECF00D91640 /* OrderGalleryImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6E946626A5CECF00D91640 /* OrderGalleryImage.swift */; }; 16 | BC6E947226A6036D00D91640 /* HeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6E947126A6036D00D91640 /* HeaderCell.swift */; }; 17 | BC6E947426A604B200D91640 /* HeaderCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6E947326A604B200D91640 /* HeaderCellViewModel.swift */; }; 18 | BCAEB916269DCCFD00DB84A8 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB915269DCCFD00DB84A8 /* FeedViewController.swift */; }; 19 | BCAEB919269DCD6D00DB84A8 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB918269DCD6D00DB84A8 /* FeedViewModel.swift */; }; 20 | BCAEB91B269DCD7D00DB84A8 /* FeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB91A269DCD7D00DB84A8 /* FeedModel.swift */; }; 21 | BCAEB920269DD14300DB84A8 /* OrderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB91F269DD14300DB84A8 /* OrderModel.swift */; }; 22 | BCAEB922269DD1C200DB84A8 /* ProductModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB921269DD1C200DB84A8 /* ProductModel.swift */; }; 23 | BCAEB926269DD6E000DB84A8 /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB925269DD6E000DB84A8 /* FeedItemCell.swift */; }; 24 | BCAEB928269DD6EB00DB84A8 /* FeedItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB927269DD6EB00DB84A8 /* FeedItemViewModel.swift */; }; 25 | BCAEB92C269DD87800DB84A8 /* OrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB92B269DD87800DB84A8 /* OrderCell.swift */; }; 26 | BCAEB92E269DD88B00DB84A8 /* OrderCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB92D269DD88B00DB84A8 /* OrderCellViewModel.swift */; }; 27 | BCAEB931269DDE3700DB84A8 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCAEB930269DDE3700DB84A8 /* CollectionExtension.swift */; }; 28 | BCB97F4A268234D700AE0968 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB97F42268234D700AE0968 /* AppDelegate.swift */; }; 29 | BCD6DCE626966F0B00D2E481 /* SharkUtils in Frameworks */ = {isa = PBXBuildFile; productRef = BCD6DCE526966F0B00D2E481 /* SharkUtils */; }; 30 | BCE7C57B26489DA200515A05 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE7C57A26489DA200515A05 /* Constants.swift */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | BC4F374526A753F900B41D71 /* StackKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = StackKit; path = ..; sourceTree = ""; }; 35 | BC5020DA2645EAE400E7E234 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | BC5020E62645EAE500E7E234 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | BC5020E92645EAE500E7E234 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | BC5020EB2645EAE500E7E234 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | BC6E946126A471AA00D91640 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; 40 | BC6E946326A5B41A00D91640 /* OrderGalleryImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderGalleryImageView.swift; sourceTree = ""; }; 41 | BC6E946626A5CECF00D91640 /* OrderGalleryImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderGalleryImage.swift; sourceTree = ""; }; 42 | BC6E947126A6036D00D91640 /* HeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderCell.swift; sourceTree = ""; }; 43 | BC6E947326A604B200D91640 /* HeaderCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderCellViewModel.swift; sourceTree = ""; }; 44 | BCAEB915269DCCFD00DB84A8 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 45 | BCAEB918269DCD6D00DB84A8 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = ""; }; 46 | BCAEB91A269DCD7D00DB84A8 /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = ""; }; 47 | BCAEB91F269DD14300DB84A8 /* OrderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderModel.swift; sourceTree = ""; }; 48 | BCAEB921269DD1C200DB84A8 /* ProductModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductModel.swift; sourceTree = ""; }; 49 | BCAEB925269DD6E000DB84A8 /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = ""; }; 50 | BCAEB927269DD6EB00DB84A8 /* FeedItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemViewModel.swift; sourceTree = ""; }; 51 | BCAEB92B269DD87800DB84A8 /* OrderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCell.swift; sourceTree = ""; }; 52 | BCAEB92D269DD88B00DB84A8 /* OrderCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCellViewModel.swift; sourceTree = ""; }; 53 | BCAEB930269DDE3700DB84A8 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; 54 | BCB97F42268234D700AE0968 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 55 | BCE7C57A26489DA200515A05 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | BC5020D72645EAE400E7E234 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | BC4F374726A7540100B41D71 /* StackKit in Frameworks */, 64 | BCD6DCE626966F0B00D2E481 /* SharkUtils in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | BC4F374326A7524300B41D71 /* Tag View */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | BC6E946126A471AA00D91640 /* TagView.swift */, 75 | ); 76 | path = "Tag View"; 77 | sourceTree = ""; 78 | }; 79 | BC5020D12645EAE400E7E234 = { 80 | isa = PBXGroup; 81 | children = ( 82 | BC4F374526A753F900B41D71 /* StackKit */, 83 | BC5020DC2645EAE400E7E234 /* Example */, 84 | BC5020DB2645EAE400E7E234 /* Products */, 85 | BC5020F32645EF5900E7E234 /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | BC5020DB2645EAE400E7E234 /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | BC5020DA2645EAE400E7E234 /* Example.app */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | BC5020DC2645EAE400E7E234 /* Example */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | BCB97F40268234D700AE0968 /* Flow */, 101 | BC6E946026A4719A00D91640 /* UI Components */, 102 | BCAEB91E269DD12300DB84A8 /* Models */, 103 | BCAEB92F269DDE2300DB84A8 /* Helpers */, 104 | BCE7C57826489C9D00515A05 /* Resources */, 105 | ); 106 | path = Example; 107 | sourceTree = ""; 108 | }; 109 | BC5020F32645EF5900E7E234 /* Frameworks */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | ); 113 | name = Frameworks; 114 | sourceTree = ""; 115 | }; 116 | BC6E946026A4719A00D91640 /* UI Components */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | BC4F374326A7524300B41D71 /* Tag View */, 120 | BC6E946526A5CEBD00D91640 /* Order Image Gallery */, 121 | ); 122 | path = "UI Components"; 123 | sourceTree = ""; 124 | }; 125 | BC6E946526A5CEBD00D91640 /* Order Image Gallery */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | BC6E946326A5B41A00D91640 /* OrderGalleryImageView.swift */, 129 | BC6E946626A5CECF00D91640 /* OrderGalleryImage.swift */, 130 | ); 131 | path = "Order Image Gallery"; 132 | sourceTree = ""; 133 | }; 134 | BC6E947026A6035C00D91640 /* Header */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | BC6E947126A6036D00D91640 /* HeaderCell.swift */, 138 | BC6E947326A604B200D91640 /* HeaderCellViewModel.swift */, 139 | ); 140 | path = "Header "; 141 | sourceTree = ""; 142 | }; 143 | BCAEB917269DCD0000DB84A8 /* Feed */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | BCAEB91D269DD11D00DB84A8 /* Controller */, 147 | BCAEB91C269DD11500DB84A8 /* Cells */, 148 | ); 149 | path = Feed; 150 | sourceTree = ""; 151 | }; 152 | BCAEB91C269DD11500DB84A8 /* Cells */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | BCAEB929269DD7B900DB84A8 /* Feed Item */, 156 | BC6E947026A6035C00D91640 /* Header */, 157 | BCAEB92A269DD86A00DB84A8 /* Order */, 158 | ); 159 | path = Cells; 160 | sourceTree = ""; 161 | }; 162 | BCAEB91D269DD11D00DB84A8 /* Controller */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | BCAEB915269DCCFD00DB84A8 /* FeedViewController.swift */, 166 | BCAEB918269DCD6D00DB84A8 /* FeedViewModel.swift */, 167 | ); 168 | path = Controller; 169 | sourceTree = ""; 170 | }; 171 | BCAEB91E269DD12300DB84A8 /* Models */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | BCAEB91A269DCD7D00DB84A8 /* FeedModel.swift */, 175 | BCAEB91F269DD14300DB84A8 /* OrderModel.swift */, 176 | BCAEB921269DD1C200DB84A8 /* ProductModel.swift */, 177 | ); 178 | path = Models; 179 | sourceTree = ""; 180 | }; 181 | BCAEB929269DD7B900DB84A8 /* Feed Item */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | BCAEB925269DD6E000DB84A8 /* FeedItemCell.swift */, 185 | BCAEB927269DD6EB00DB84A8 /* FeedItemViewModel.swift */, 186 | ); 187 | path = "Feed Item"; 188 | sourceTree = ""; 189 | }; 190 | BCAEB92A269DD86A00DB84A8 /* Order */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | BCAEB92B269DD87800DB84A8 /* OrderCell.swift */, 194 | BCAEB92D269DD88B00DB84A8 /* OrderCellViewModel.swift */, 195 | ); 196 | path = "Order "; 197 | sourceTree = ""; 198 | }; 199 | BCAEB92F269DDE2300DB84A8 /* Helpers */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | BCAEB930269DDE3700DB84A8 /* CollectionExtension.swift */, 203 | ); 204 | path = Helpers; 205 | sourceTree = ""; 206 | }; 207 | BCB97F40268234D700AE0968 /* Flow */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | BCB97F41268234D700AE0968 /* App Delegate */, 211 | BCAEB917269DCD0000DB84A8 /* Feed */, 212 | ); 213 | path = Flow; 214 | sourceTree = ""; 215 | }; 216 | BCB97F41268234D700AE0968 /* App Delegate */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | BCB97F42268234D700AE0968 /* AppDelegate.swift */, 220 | ); 221 | path = "App Delegate"; 222 | sourceTree = ""; 223 | }; 224 | BCE7C57826489C9D00515A05 /* Resources */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | BCE7C57A26489DA200515A05 /* Constants.swift */, 228 | BC5020E62645EAE500E7E234 /* Assets.xcassets */, 229 | BC5020E82645EAE500E7E234 /* LaunchScreen.storyboard */, 230 | BC5020EB2645EAE500E7E234 /* Info.plist */, 231 | ); 232 | path = Resources; 233 | sourceTree = ""; 234 | }; 235 | /* End PBXGroup section */ 236 | 237 | /* Begin PBXNativeTarget section */ 238 | BC5020D92645EAE400E7E234 /* Example */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = BC5020EE2645EAE500E7E234 /* Build configuration list for PBXNativeTarget "Example" */; 241 | buildPhases = ( 242 | BC5020D62645EAE400E7E234 /* Sources */, 243 | BC5020D72645EAE400E7E234 /* Frameworks */, 244 | BC5020D82645EAE400E7E234 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | ); 250 | name = Example; 251 | packageProductDependencies = ( 252 | BCD6DCE526966F0B00D2E481 /* SharkUtils */, 253 | BC4F374626A7540100B41D71 /* StackKit */, 254 | ); 255 | productName = Example; 256 | productReference = BC5020DA2645EAE400E7E234 /* Example.app */; 257 | productType = "com.apple.product-type.application"; 258 | }; 259 | /* End PBXNativeTarget section */ 260 | 261 | /* Begin PBXProject section */ 262 | BC5020D22645EAE400E7E234 /* Project object */ = { 263 | isa = PBXProject; 264 | attributes = { 265 | LastSwiftUpdateCheck = 1250; 266 | LastUpgradeCheck = 1250; 267 | TargetAttributes = { 268 | BC5020D92645EAE400E7E234 = { 269 | CreatedOnToolsVersion = 12.5; 270 | }; 271 | }; 272 | }; 273 | buildConfigurationList = BC5020D52645EAE400E7E234 /* Build configuration list for PBXProject "Example" */; 274 | compatibilityVersion = "Xcode 9.3"; 275 | developmentRegion = en; 276 | hasScannedForEncodings = 0; 277 | knownRegions = ( 278 | en, 279 | Base, 280 | ); 281 | mainGroup = BC5020D12645EAE400E7E234; 282 | packageReferences = ( 283 | BCD6DCE426966F0B00D2E481 /* XCRemoteSwiftPackageReference "ios-shark-utils" */, 284 | ); 285 | productRefGroup = BC5020DB2645EAE400E7E234 /* Products */; 286 | projectDirPath = ""; 287 | projectRoot = ""; 288 | targets = ( 289 | BC5020D92645EAE400E7E234 /* Example */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | BC5020D82645EAE400E7E234 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | BC5020EA2645EAE500E7E234 /* LaunchScreen.storyboard in Resources */, 300 | BC5020E72645EAE500E7E234 /* Assets.xcassets in Resources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | /* End PBXResourcesBuildPhase section */ 305 | 306 | /* Begin PBXSourcesBuildPhase section */ 307 | BC5020D62645EAE400E7E234 /* Sources */ = { 308 | isa = PBXSourcesBuildPhase; 309 | buildActionMask = 2147483647; 310 | files = ( 311 | BCAEB928269DD6EB00DB84A8 /* FeedItemViewModel.swift in Sources */, 312 | BC6E946726A5CECF00D91640 /* OrderGalleryImage.swift in Sources */, 313 | BCAEB92E269DD88B00DB84A8 /* OrderCellViewModel.swift in Sources */, 314 | BCAEB916269DCCFD00DB84A8 /* FeedViewController.swift in Sources */, 315 | BC6E947226A6036D00D91640 /* HeaderCell.swift in Sources */, 316 | BCAEB920269DD14300DB84A8 /* OrderModel.swift in Sources */, 317 | BCAEB92C269DD87800DB84A8 /* OrderCell.swift in Sources */, 318 | BCAEB931269DDE3700DB84A8 /* CollectionExtension.swift in Sources */, 319 | BC6E946226A471AA00D91640 /* TagView.swift in Sources */, 320 | BC6E947426A604B200D91640 /* HeaderCellViewModel.swift in Sources */, 321 | BCAEB922269DD1C200DB84A8 /* ProductModel.swift in Sources */, 322 | BCB97F4A268234D700AE0968 /* AppDelegate.swift in Sources */, 323 | BCAEB919269DCD6D00DB84A8 /* FeedViewModel.swift in Sources */, 324 | BCAEB926269DD6E000DB84A8 /* FeedItemCell.swift in Sources */, 325 | BCAEB91B269DCD7D00DB84A8 /* FeedModel.swift in Sources */, 326 | BCE7C57B26489DA200515A05 /* Constants.swift in Sources */, 327 | BC6E946426A5B41A00D91640 /* OrderGalleryImageView.swift in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | /* End PBXSourcesBuildPhase section */ 332 | 333 | /* Begin PBXVariantGroup section */ 334 | BC5020E82645EAE500E7E234 /* LaunchScreen.storyboard */ = { 335 | isa = PBXVariantGroup; 336 | children = ( 337 | BC5020E92645EAE500E7E234 /* Base */, 338 | ); 339 | name = LaunchScreen.storyboard; 340 | sourceTree = ""; 341 | }; 342 | /* End PBXVariantGroup section */ 343 | 344 | /* Begin XCBuildConfiguration section */ 345 | BC5020EC2645EAE500E7E234 /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ALWAYS_SEARCH_USER_PATHS = NO; 349 | CLANG_ANALYZER_NONNULL = YES; 350 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 352 | CLANG_CXX_LIBRARY = "libc++"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_ENABLE_OBJC_WEAK = YES; 356 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 357 | CLANG_WARN_BOOL_CONVERSION = YES; 358 | CLANG_WARN_COMMA = YES; 359 | CLANG_WARN_CONSTANT_CONVERSION = YES; 360 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 361 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 362 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INFINITE_RECURSION = YES; 366 | CLANG_WARN_INT_CONVERSION = YES; 367 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 369 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 372 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 373 | CLANG_WARN_STRICT_PROTOTYPES = YES; 374 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 375 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 376 | CLANG_WARN_UNREACHABLE_CODE = YES; 377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 378 | COPY_PHASE_STRIP = NO; 379 | DEBUG_INFORMATION_FORMAT = dwarf; 380 | ENABLE_STRICT_OBJC_MSGSEND = YES; 381 | ENABLE_TESTABILITY = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu11; 383 | GCC_DYNAMIC_NO_PIC = NO; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_OPTIMIZATION_LEVEL = 0; 386 | GCC_PREPROCESSOR_DEFINITIONS = ( 387 | "DEBUG=1", 388 | "$(inherited)", 389 | ); 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 397 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 398 | MTL_FAST_MATH = YES; 399 | ONLY_ACTIVE_ARCH = YES; 400 | SDKROOT = iphoneos; 401 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 403 | }; 404 | name = Debug; 405 | }; 406 | BC5020ED2645EAE500E7E234 /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_ANALYZER_NONNULL = YES; 411 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 412 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 413 | CLANG_CXX_LIBRARY = "libc++"; 414 | CLANG_ENABLE_MODULES = YES; 415 | CLANG_ENABLE_OBJC_ARC = YES; 416 | CLANG_ENABLE_OBJC_WEAK = YES; 417 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 418 | CLANG_WARN_BOOL_CONVERSION = YES; 419 | CLANG_WARN_COMMA = YES; 420 | CLANG_WARN_CONSTANT_CONVERSION = YES; 421 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 422 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 423 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 424 | CLANG_WARN_EMPTY_BODY = YES; 425 | CLANG_WARN_ENUM_CONVERSION = YES; 426 | CLANG_WARN_INFINITE_RECURSION = YES; 427 | CLANG_WARN_INT_CONVERSION = YES; 428 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 429 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 430 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 432 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 433 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 434 | CLANG_WARN_STRICT_PROTOTYPES = YES; 435 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 436 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 437 | CLANG_WARN_UNREACHABLE_CODE = YES; 438 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 439 | COPY_PHASE_STRIP = NO; 440 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 441 | ENABLE_NS_ASSERTIONS = NO; 442 | ENABLE_STRICT_OBJC_MSGSEND = YES; 443 | GCC_C_LANGUAGE_STANDARD = gnu11; 444 | GCC_NO_COMMON_BLOCKS = YES; 445 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 446 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 447 | GCC_WARN_UNDECLARED_SELECTOR = YES; 448 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 449 | GCC_WARN_UNUSED_FUNCTION = YES; 450 | GCC_WARN_UNUSED_VARIABLE = YES; 451 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 452 | MTL_ENABLE_DEBUG_INFO = NO; 453 | MTL_FAST_MATH = YES; 454 | SDKROOT = iphoneos; 455 | SWIFT_COMPILATION_MODE = wholemodule; 456 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 457 | VALIDATE_PRODUCT = YES; 458 | }; 459 | name = Release; 460 | }; 461 | BC5020EF2645EAE500E7E234 /* Debug */ = { 462 | isa = XCBuildConfiguration; 463 | buildSettings = { 464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 465 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 466 | CODE_SIGN_STYLE = Automatic; 467 | DEVELOPMENT_TEAM = X52QJY6S22; 468 | INFOPLIST_FILE = Example/Resources/Info.plist; 469 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 470 | LD_RUNPATH_SEARCH_PATHS = ( 471 | "$(inherited)", 472 | "@executable_path/Frameworks", 473 | ); 474 | PRODUCT_BUNDLE_IDENTIFIER = com.russ.testing; 475 | PRODUCT_NAME = "$(TARGET_NAME)"; 476 | SWIFT_VERSION = 5.0; 477 | TARGETED_DEVICE_FAMILY = "1,2"; 478 | }; 479 | name = Debug; 480 | }; 481 | BC5020F02645EAE500E7E234 /* Release */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 485 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 486 | CODE_SIGN_STYLE = Automatic; 487 | DEVELOPMENT_TEAM = X52QJY6S22; 488 | INFOPLIST_FILE = Example/Resources/Info.plist; 489 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 490 | LD_RUNPATH_SEARCH_PATHS = ( 491 | "$(inherited)", 492 | "@executable_path/Frameworks", 493 | ); 494 | PRODUCT_BUNDLE_IDENTIFIER = com.russ.testing; 495 | PRODUCT_NAME = "$(TARGET_NAME)"; 496 | SWIFT_VERSION = 5.0; 497 | TARGETED_DEVICE_FAMILY = "1,2"; 498 | }; 499 | name = Release; 500 | }; 501 | /* End XCBuildConfiguration section */ 502 | 503 | /* Begin XCConfigurationList section */ 504 | BC5020D52645EAE400E7E234 /* Build configuration list for PBXProject "Example" */ = { 505 | isa = XCConfigurationList; 506 | buildConfigurations = ( 507 | BC5020EC2645EAE500E7E234 /* Debug */, 508 | BC5020ED2645EAE500E7E234 /* Release */, 509 | ); 510 | defaultConfigurationIsVisible = 0; 511 | defaultConfigurationName = Release; 512 | }; 513 | BC5020EE2645EAE500E7E234 /* Build configuration list for PBXNativeTarget "Example" */ = { 514 | isa = XCConfigurationList; 515 | buildConfigurations = ( 516 | BC5020EF2645EAE500E7E234 /* Debug */, 517 | BC5020F02645EAE500E7E234 /* Release */, 518 | ); 519 | defaultConfigurationIsVisible = 0; 520 | defaultConfigurationName = Release; 521 | }; 522 | /* End XCConfigurationList section */ 523 | 524 | /* Begin XCRemoteSwiftPackageReference section */ 525 | BCD6DCE426966F0B00D2E481 /* XCRemoteSwiftPackageReference "ios-shark-utils" */ = { 526 | isa = XCRemoteSwiftPackageReference; 527 | repositoryURL = "git@github.com:gymshark/ios-shark-utils.git"; 528 | requirement = { 529 | kind = exactVersion; 530 | version = 1.0.6; 531 | }; 532 | }; 533 | /* End XCRemoteSwiftPackageReference section */ 534 | 535 | /* Begin XCSwiftPackageProductDependency section */ 536 | BC4F374626A7540100B41D71 /* StackKit */ = { 537 | isa = XCSwiftPackageProductDependency; 538 | productName = StackKit; 539 | }; 540 | BCD6DCE526966F0B00D2E481 /* SharkUtils */ = { 541 | isa = XCSwiftPackageProductDependency; 542 | package = BCD6DCE426966F0B00D2E481 /* XCRemoteSwiftPackageReference "ios-shark-utils" */; 543 | productName = SharkUtils; 544 | }; 545 | /* End XCSwiftPackageProductDependency section */ 546 | }; 547 | rootObject = BC5020D22645EAE400E7E234 /* Project object */; 548 | } 549 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ConstraintKit", 6 | "repositoryURL": "https://github.com/gymshark/ios-constraint-kit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "404baf28a47a5cbbb9fcb59ef8c7be605e394c4a", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "SharkUtils", 15 | "repositoryURL": "git@github.com:gymshark/ios-shark-utils.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "aef1ed76f2d69b8be1c0a0ed664dfad2fd55aa1f", 19 | "version": "1.0.6" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/Example/Flow/App Delegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 07/05/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | window?.rootViewController = FeedViewController(viewModel: FeedViewModel()) 19 | window?.makeKeyAndVisible() 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Feed Item/FeedItemCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItemCell.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import ConstraintKit 11 | 12 | final class FeedItemCell: UITableViewCell { 13 | 14 | // MARK: - UI 15 | 16 | private let cardImage = UIImageView().with { 17 | $0.setCornerRadius(12) 18 | $0.backgroundColor = .gray 19 | $0.contentMode = .scaleAspectFill 20 | $0.clipsToBounds = true 21 | } 22 | 23 | private let titleLabel = UILabel().with { 24 | $0.font = .systemFont(ofSize: 18, weight: .heavy) 25 | $0.textColor = .white 26 | $0.numberOfLines = 2 27 | } 28 | 29 | private let subtitleLabel = UILabel().with { 30 | $0.font = .systemFont(ofSize: 15, weight: .regular) 31 | $0.textColor = .white 32 | } 33 | 34 | private let button = UIImageView(image: UIImage(named: "button-icon")) 35 | 36 | private let tagView = TagView() 37 | 38 | private lazy var tagStack = HStack { 39 | tagView 40 | Spacer() 41 | } 42 | 43 | // MARK: - Init 44 | 45 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 46 | super.init(style: style, reuseIdentifier: reuseIdentifier) 47 | setConstraints() 48 | configure() 49 | } 50 | 51 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 52 | 53 | // MARK: - ViewModel 54 | 55 | var viewModel: FeedItemViewModel? { 56 | didSet { 57 | guard let viewModel = viewModel else { return } 58 | titleLabel.text = viewModel.titleText 59 | subtitleLabel.text = viewModel.subtitleText 60 | tagView.text = viewModel.tagText 61 | tagStack.isHidden = viewModel.isTagHidden 62 | cardImage.image = UIImage(named: viewModel.imageName) 63 | } 64 | } 65 | 66 | // MARK: - Configure 67 | 68 | private func configure(){ 69 | backgroundColor = .clear 70 | contentView.backgroundColor = .clear 71 | selectionStyle = .none 72 | } 73 | 74 | private func setConstraints(){ 75 | contentView.ZStack { 76 | cardImage 77 | VStack { 78 | tagStack 79 | Spacer() 80 | 81 | HStack { 82 | VStack { 83 | titleLabel 84 | Spacer(h: 6) 85 | subtitleLabel 86 | } 87 | Spacer(w: 10) 88 | button.withSquare(50) 89 | }.alignment(.center) 90 | 91 | }.margin(.all(Constants.layout.contentMargin)) 92 | 93 | }.margin(.insets(top: Constants.layout.spacing, left: Constants.layout.tableMargin, right: Constants.layout.tableMargin)) 94 | .withAspectRatio(0.9, priority: .init(999)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Feed Item/FeedItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItemViewModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FeedItemViewModel { 11 | 12 | // MARK: - Dependencies 13 | 14 | private let feedItem: FeedModel 15 | 16 | // MARK: - Init 17 | 18 | init(feedItem: FeedModel){ 19 | self.feedItem = feedItem 20 | } 21 | 22 | // MARK: - Interface 23 | 24 | var titleText: String { 25 | return feedItem.title.uppercased() 26 | } 27 | 28 | var subtitleText: String? { 29 | return feedItem.subtitle 30 | } 31 | 32 | var imageName: String { 33 | return feedItem.image ?? "" 34 | } 35 | 36 | var tagText: String? { 37 | return feedItem.tag?.uppercased() 38 | } 39 | 40 | var isTagHidden: Bool { 41 | return tagText == nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Header /HeaderCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderCell.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 19/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import SharkUtils 11 | import ConstraintKit 12 | 13 | final class HeaderCell: UITableViewCell { 14 | 15 | // MARK: - UI 16 | 17 | private let headlineLabel = UILabel().with { 18 | $0.font = .systemFont(ofSize: 17, weight: .heavy) 19 | $0.textColor = .black 20 | } 21 | 22 | private let initialsLabel = UILabel().with { 23 | $0.font = .systemFont(ofSize: 15, weight: .heavy) 24 | $0.textColor = .white 25 | $0.textAlignment = .center 26 | } 27 | 28 | private let initialsView = UIView().with { 29 | $0.backgroundColor = .darkGray 30 | $0.setCornerRadius(18) 31 | $0.clipsToBounds = true 32 | } 33 | 34 | private let searchButton = UIImageView().with({ 35 | $0.image = UIImage(named: "search-icon") 36 | $0.contentMode = .scaleAspectFit 37 | }) 38 | 39 | // MARK: - Init 40 | 41 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 42 | super.init(style: style, reuseIdentifier: reuseIdentifier) 43 | setConstraints() 44 | configure() 45 | } 46 | 47 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 48 | 49 | // MARK: - ViewModel 50 | 51 | var viewModel: HeaderCellViewModel? { 52 | didSet { 53 | headlineLabel.text = viewModel?.headlineText 54 | initialsLabel.text = viewModel?.initialsText 55 | } 56 | } 57 | 58 | // MARK: - Configure 59 | 60 | private func configure(){ 61 | backgroundColor = .clear 62 | contentView.backgroundColor = .clear 63 | selectionStyle = .none 64 | } 65 | 66 | private func setConstraints(){ 67 | 68 | initialsView.VStack { 69 | initialsLabel 70 | } 71 | 72 | contentView.HStack { 73 | initialsView.withSquare(36) 74 | Spacer(w: 10) 75 | headlineLabel 76 | searchButton.withSquare(20) 77 | }.margin(.insets(top: Constants.layout.spacing, left: Constants.layout.tableMargin, right: Constants.layout.tableMargin)) 78 | .alignment(.center) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Header /HeaderCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderCellViewModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 19/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct HeaderCellViewModel { 11 | 12 | var headlineText: String { 13 | return "HI, RUSSELL" 14 | } 15 | 16 | var initialsText: String { 17 | return "RW" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Order /OrderCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderCell.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import ConstraintKit 11 | 12 | final class OrderCell: UITableViewCell { 13 | 14 | // MARK: - UI 15 | 16 | private let card = UIView().with { 17 | $0.setCornerRadius(12) 18 | $0.backgroundColor = .white 19 | } 20 | 21 | private let titleLabel = UILabel().with { 22 | $0.font = .systemFont(ofSize: 18, weight: .heavy) 23 | $0.textColor = .black 24 | $0.numberOfLines = 2 25 | } 26 | 27 | private let subtitleLabel = UILabel().with { 28 | $0.font = .systemFont(ofSize: 15, weight: .regular) 29 | $0.textColor = .black 30 | } 31 | 32 | private let statusBar = UIView().with { 33 | $0.backgroundColor = Constants.colors.tint 34 | $0.setCornerRadius(3) 35 | } 36 | 37 | private let gallery = OrderGalleryImageView() 38 | 39 | private let moreButton = UIImageView().with({ 40 | $0.image = UIImage(named: "more-icon") 41 | $0.contentMode = .scaleAspectFit 42 | }) 43 | 44 | // MARK: - Init 45 | 46 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 47 | super.init(style: style, reuseIdentifier: reuseIdentifier) 48 | setConstraints() 49 | configure() 50 | } 51 | 52 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 53 | 54 | // MARK: - ViewModel 55 | 56 | var viewModel: OrderCellViewModel? { 57 | didSet { 58 | guard let viewModel = viewModel else { return } 59 | titleLabel.text = viewModel.titleText 60 | subtitleLabel.text = viewModel.subtitleText 61 | gallery.images = viewModel.images 62 | } 63 | } 64 | 65 | // MARK: - Configure 66 | 67 | private func configure(){ 68 | backgroundColor = .clear 69 | contentView.backgroundColor = .clear 70 | selectionStyle = .none 71 | } 72 | 73 | private func setConstraints(){ 74 | contentView.ZStack { 75 | card 76 | VStack(spacing: 5) { 77 | 78 | HStack { 79 | titleLabel 80 | moreButton.withWidth(26) 81 | }.alignment(.center) 82 | 83 | subtitleLabel 84 | Spacer(h: 3) 85 | statusBar.withHeight(6) 86 | Spacer(h: 5) 87 | gallery 88 | }.margin(.all(Constants.layout.contentMargin)) 89 | 90 | }.margin(.insets(top: Constants.layout.spacing, left: Constants.layout.tableMargin, right: Constants.layout.tableMargin)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Cells/Order /OrderCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderCellViewModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct OrderCellViewModel { 11 | 12 | // MARK: - Dependenies 13 | 14 | private let order: OrderModel 15 | 16 | // MARK: - Init 17 | 18 | init(order: OrderModel){ 19 | self.order = order 20 | } 21 | 22 | // MARK: - Interface 23 | 24 | var titleText: String { 25 | return ("IT'S " + order.status).uppercased() 26 | } 27 | 28 | var subtitleText: String { 29 | let df = DateFormatter() 30 | df.dateFormat = "dd MMM yy" 31 | return "Ordered on " + df.string(from: order.orderDate) 32 | } 33 | 34 | var images: [String]? { 35 | return ["tshirt", "top", "hoodie", "shorts", "shorts", "shorts"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Controller/FeedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedViewController.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | 11 | final class FeedViewController: UIViewController { 12 | 13 | // MARK: - UI 14 | 15 | private lazy var tableView = UITableView().with { 16 | $0.register(FeedItemCell.self, forCellReuseIdentifier: FeedItemCell.identifier) 17 | $0.register(OrderCell.self, forCellReuseIdentifier: OrderCell.identifier) 18 | $0.register(HeaderCell.self, forCellReuseIdentifier: HeaderCell.identifier) 19 | $0.rowHeight = UITableView.automaticDimension 20 | $0.delegate = self 21 | $0.dataSource = self 22 | $0.backgroundColor = .clear 23 | $0.separatorStyle = .none 24 | } 25 | 26 | // MARK: - Dependencies 27 | 28 | private let viewModel: FeedViewModel 29 | 30 | // MARK: - Init 31 | 32 | init(viewModel: FeedViewModel){ 33 | self.viewModel = viewModel 34 | super.init(nibName: nil, bundle: nil) 35 | setConstraints() 36 | configure() 37 | } 38 | 39 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 40 | 41 | // MARK: - Configure 42 | 43 | private func setConstraints(){ 44 | view.VStack(useSafeArea: false) { 45 | tableView 46 | } 47 | } 48 | 49 | private func configure(){ 50 | view.backgroundColor = Constants.colors.background 51 | } 52 | } 53 | 54 | extension FeedViewController: UITableViewDelegate, UITableViewDataSource { 55 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 56 | guard let item = viewModel.cellItem(for: indexPath) else { return UITableViewCell() } 57 | 58 | switch item { 59 | case .header(let vm): 60 | let cell = tableView.dequeueReusableCell(withIdentifier: HeaderCell.identifier, for: indexPath) as! HeaderCell 61 | cell.viewModel = vm 62 | return cell 63 | 64 | case .feedItem(let vm): 65 | let cell = tableView.dequeueReusableCell(withIdentifier: FeedItemCell.identifier, for: indexPath) as! FeedItemCell 66 | cell.viewModel = vm 67 | return cell 68 | 69 | case .order(let vm): 70 | let cell = tableView.dequeueReusableCell(withIdentifier: OrderCell.identifier, for: indexPath) as! OrderCell 71 | cell.viewModel = vm 72 | return cell 73 | } 74 | } 75 | 76 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 77 | return viewModel.numberOfRows 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Example/Flow/Feed/Controller/FeedViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedViewModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | import SharkUtils 10 | 11 | enum Feeditem { 12 | case header(HeaderCellViewModel) 13 | case feedItem(FeedItemViewModel) 14 | case order(OrderCellViewModel) 15 | } 16 | 17 | final class FeedViewModel { 18 | 19 | // MARK: - Interface 20 | 21 | func cellItem(for indexPath: IndexPath) -> Feeditem? { 22 | return items[safe: indexPath.row] 23 | } 24 | 25 | var numberOfRows: Int { 26 | return items.count 27 | } 28 | 29 | var numberOfSections: Int { 30 | return 1 31 | } 32 | } 33 | 34 | extension FeedViewModel { 35 | 36 | // MARK: - Items 37 | 38 | private var items: [Feeditem] { 39 | var items: [Feeditem] = [.header(HeaderCellViewModel())] 40 | 41 | var feedItems: [Feeditem] = feedVMs.compactMap({ .feedItem($0) }) 42 | 43 | if let orderVM = orderViewModel { 44 | feedItems.insert(.order(orderVM), at: (feedItems.isEmpty) ? 0 : 1) 45 | } 46 | 47 | items.append(contentsOf: feedItems) 48 | 49 | return items 50 | } 51 | 52 | // MARK: - View Models 53 | 54 | private var orderViewModel: OrderCellViewModel? { 55 | guard let order = myOrder else { return nil } 56 | return OrderCellViewModel(order: order) 57 | } 58 | 59 | private var feedVMs: [FeedItemViewModel] { 60 | return feedModels.compactMap({ FeedItemViewModel(feedItem: $0) }) 61 | } 62 | } 63 | 64 | extension FeedViewModel { 65 | 66 | // MARK: - Stub data 67 | 68 | private var feedModels: [FeedModel] { 69 | return [ 70 | FeedModel(title: "The future is functional", subtitle: "Get after your goals in new Apex", 71 | tag: "new releases", image: "functional-male"), 72 | FeedModel(title: "Press reset", subtitle: "The newest collection is here", image: "reset"), 73 | FeedModel(title: "The future is functional", subtitle: "Get after your goals in new Apex", 74 | tag: "new releases", image: "functional-female"), 75 | FeedModel(title: "Raise the bar", subtitle: "Level up in Vital Rise", image: "raise"), 76 | FeedModel(title: "Must haves", subtitle: "Your key piece inspo", image: "must-have"), 77 | FeedModel(title: "Take on the heat", subtitle: "In summer activewear", image: "flex") 78 | ] 79 | } 80 | 81 | private var myOrder: OrderModel? { 82 | return OrderModel(status: "FUFILLED", 83 | items: [ProductModel(productTitle: "Arrival Shorts", image: "shorts"), 84 | ProductModel(productTitle: "Arrival T-Shirt", image: "tshirt")]) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/Example/Helpers/CollectionExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionExtension.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | extension Collection { 11 | // Return element if within the bounds, else rerturn nil 12 | subscript (safe index: Index) -> Element? { 13 | return indices.contains(index) ? self[index] : nil 14 | } 15 | } 16 | 17 | extension UITableViewCell { 18 | static var identifier: String { 19 | return String(describing: self) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Models/FeedModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FeedModel { 11 | 12 | let id: String 13 | let title: String 14 | let subtitle: String? 15 | let tag: String? 16 | let image: String? 17 | 18 | init(id: String = UUID().uuidString, title: String, subtitle: String? = nil, tag: String? = nil, image: String?) { 19 | self.id = id 20 | self.title = title 21 | self.subtitle = subtitle 22 | self.tag = tag 23 | self.image = image 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Example/Example/Models/OrderModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct OrderModel { 11 | 12 | let id: String 13 | let status: String 14 | let orderDate: Date 15 | let items: [ProductModel] 16 | 17 | init(id: String = UUID().uuidString, 18 | status: String, 19 | orderDate: Date = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date(), 20 | items: [ProductModel]) { 21 | 22 | self.id = id 23 | self.status = status 24 | self.orderDate = orderDate 25 | self.items = items 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Example/Example/Models/ProductModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductModel.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 13/07/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ProductModel { 11 | 12 | let id: String 13 | let productTitle: String 14 | let image: String 15 | 16 | init(id: String = UUID().uuidString, productTitle: String, image: String) { 17 | self.id = id 18 | self.productTitle = productTitle 19 | self.image = image 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/button-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "button-icon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/button-icon.imageset/button-icon.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /ExtGState << /E1 << /ca 0.170000 >> >> >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | q 14 | /E1 gs 15 | 1.000000 0.000000 -0.000000 1.000000 11.000000 -7.000000 cm 16 | 1.000000 1.000000 1.000000 scn 17 | 44.000000 30.000000 m 18 | 25.222319 30.000000 10.000000 45.222321 10.000000 64.000000 c 19 | -10.000000 64.000000 l 20 | -10.000000 34.176620 14.176624 10.000000 44.000000 10.000000 c 21 | 44.000000 30.000000 l 22 | h 23 | 78.000000 64.000000 m 24 | 78.000000 45.222321 62.777679 30.000000 44.000000 30.000000 c 25 | 44.000000 10.000000 l 26 | 73.823380 10.000000 98.000000 34.176620 98.000000 64.000000 c 27 | 78.000000 64.000000 l 28 | h 29 | 44.000000 98.000000 m 30 | 62.777679 98.000000 78.000000 82.777679 78.000000 64.000000 c 31 | 98.000000 64.000000 l 32 | 98.000000 93.823380 73.823380 118.000000 44.000000 118.000000 c 33 | 44.000000 98.000000 l 34 | h 35 | 44.000000 118.000000 m 36 | 14.176624 118.000000 -10.000000 93.823380 -10.000000 64.000000 c 37 | 10.000000 64.000000 l 38 | 10.000000 82.777679 25.222319 98.000000 44.000000 98.000000 c 39 | 44.000000 118.000000 l 40 | h 41 | f 42 | n 43 | Q 44 | q 45 | 1.000000 0.000000 -0.000000 1.000000 11.000000 -7.000000 cm 46 | 1.000000 1.000000 1.000000 scn 47 | 0.000000 64.000000 m 48 | 0.000000 88.300529 19.699472 108.000000 44.000000 108.000000 c 49 | 44.000000 108.000000 l 50 | 68.300529 108.000000 88.000000 88.300529 88.000000 64.000000 c 51 | 88.000000 64.000000 l 52 | 88.000000 39.699471 68.300529 20.000000 44.000000 20.000000 c 53 | 44.000000 20.000000 l 54 | 19.699472 20.000000 0.000000 39.699471 0.000000 64.000000 c 55 | 0.000000 64.000000 l 56 | h 57 | f 58 | n 59 | Q 60 | Q 61 | q 62 | 0.697444 0.716640 -0.697444 0.716640 60.303665 37.165039 cm 63 | 0.304167 0.286424 0.286424 scn 64 | 16.738838 27.677734 m 65 | 16.738838 29.058447 15.619550 30.177734 14.238838 30.177734 c 66 | 12.858126 30.177734 11.738838 29.058447 11.738838 27.677734 c 67 | 16.738838 27.677734 l 68 | h 69 | 14.238838 13.295068 m 70 | 14.238838 10.795069 l 71 | 15.619550 10.795069 16.738838 11.914356 16.738838 13.295068 c 72 | 14.238838 13.295068 l 73 | h 74 | 0.000000 15.795068 m 75 | -1.380712 15.795068 -2.500000 14.675780 -2.500000 13.295068 c 76 | -2.500000 11.914356 -1.380712 10.795069 0.000000 10.795069 c 77 | 0.000000 15.795068 l 78 | h 79 | 11.738838 27.677734 m 80 | 11.738838 13.295068 l 81 | 16.738838 13.295068 l 82 | 16.738838 27.677734 l 83 | 11.738838 27.677734 l 84 | h 85 | 14.238838 15.795068 m 86 | 0.000000 15.795068 l 87 | 0.000000 10.795069 l 88 | 14.238838 10.795069 l 89 | 14.238838 15.795068 l 90 | h 91 | f 92 | n 93 | Q 94 | 95 | endstream 96 | endobj 97 | 98 | 3 0 obj 99 | 2161 100 | endobj 101 | 102 | 4 0 obj 103 | << /Annots [] 104 | /Type /Page 105 | /MediaBox [ 0.000000 0.000000 109.000000 112.000000 ] 106 | /Resources 1 0 R 107 | /Contents 2 0 R 108 | /Parent 5 0 R 109 | >> 110 | endobj 111 | 112 | 5 0 obj 113 | << /Kids [ 4 0 R ] 114 | /Count 1 115 | /Type /Pages 116 | >> 117 | endobj 118 | 119 | 6 0 obj 120 | << /Type /Catalog 121 | /Pages 5 0 R 122 | >> 123 | endobj 124 | 125 | xref 126 | 0 7 127 | 0000000000 65535 f 128 | 0000000010 00000 n 129 | 0000000074 00000 n 130 | 0000002291 00000 n 131 | 0000002314 00000 n 132 | 0000002489 00000 n 133 | 0000002563 00000 n 134 | trailer 135 | << /ID [ (some) (id) ] 136 | /Root 6 0 R 137 | /Size 7 138 | >> 139 | startxref 140 | 2622 141 | %%EOF -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/more-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "more-icon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/more-icon.imageset/more-icon.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 4.000000 2.000000 m 16 | 4.000000 0.895431 3.104569 0.000000 2.000000 0.000000 c 17 | 0.895431 0.000000 0.000000 0.895431 0.000000 2.000000 c 18 | 0.000000 3.104569 0.895431 4.000000 2.000000 4.000000 c 19 | 3.104569 4.000000 4.000000 3.104569 4.000000 2.000000 c 20 | h 21 | f 22 | n 23 | Q 24 | q 25 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 26 | 0.000000 0.000000 0.000000 scn 27 | 10.000000 2.000000 m 28 | 10.000000 0.895431 9.104569 0.000000 8.000000 0.000000 c 29 | 6.895431 0.000000 6.000000 0.895431 6.000000 2.000000 c 30 | 6.000000 3.104569 6.895431 4.000000 8.000000 4.000000 c 31 | 9.104569 4.000000 10.000000 3.104569 10.000000 2.000000 c 32 | h 33 | f 34 | n 35 | Q 36 | q 37 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 38 | 0.000000 0.000000 0.000000 scn 39 | 14.000000 0.000000 m 40 | 15.104568 0.000000 16.000000 0.895431 16.000000 2.000000 c 41 | 16.000000 3.104569 15.104568 4.000000 14.000000 4.000000 c 42 | 12.895430 4.000000 12.000000 3.104569 12.000000 2.000000 c 43 | 12.000000 0.895431 12.895430 0.000000 14.000000 0.000000 c 44 | h 45 | f 46 | n 47 | Q 48 | 49 | endstream 50 | endobj 51 | 52 | 3 0 obj 53 | 1074 54 | endobj 55 | 56 | 4 0 obj 57 | << /Annots [] 58 | /Type /Page 59 | /MediaBox [ 0.000000 0.000000 16.000000 4.000000 ] 60 | /Resources 1 0 R 61 | /Contents 2 0 R 62 | /Parent 5 0 R 63 | >> 64 | endobj 65 | 66 | 5 0 obj 67 | << /Kids [ 4 0 R ] 68 | /Count 1 69 | /Type /Pages 70 | >> 71 | endobj 72 | 73 | 6 0 obj 74 | << /Type /Catalog 75 | /Pages 5 0 R 76 | >> 77 | endobj 78 | 79 | xref 80 | 0 7 81 | 0000000000 65535 f 82 | 0000000010 00000 n 83 | 0000000034 00000 n 84 | 0000001164 00000 n 85 | 0000001187 00000 n 86 | 0000001359 00000 n 87 | 0000001433 00000 n 88 | trailer 89 | << /ID [ (some) (id) ] 90 | /Root 6 0 R 91 | /Size 7 92 | >> 93 | startxref 94 | 1492 95 | %%EOF -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/search-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "search-icon.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Assets/search-icon.imageset/search-icon.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | -1.000000 -0.000000 -0.000000 1.000000 26.000000 -1.208801 cm 14 | 0.000000 0.000000 0.000000 scn 15 | 9.157823 10.366623 m 16 | 5.934361 13.590084 5.934361 18.816347 9.157823 22.039808 c 17 | 12.381284 25.263271 17.607548 25.263271 20.831009 22.039808 c 18 | 24.054470 18.816347 24.054470 13.590084 20.831009 10.366623 c 19 | 17.607548 7.143162 12.381284 7.143162 9.157823 10.366623 c 20 | h 21 | 7.212290 23.985340 m 22 | 3.243151 20.016201 2.939497 13.769619 6.301330 9.452015 c 23 | 6.280283 9.433380 6.259669 9.414001 6.239525 9.393858 c 24 | 0.402932 3.557262 l 25 | -0.134311 3.020020 -0.134311 2.148975 0.402932 1.611732 c 26 | 0.940175 1.074490 1.811220 1.074490 2.348463 1.611732 c 27 | 8.185057 7.448326 l 28 | 8.205199 7.468470 8.224580 7.489084 8.243214 7.510130 c 29 | 12.560818 4.148296 18.807400 4.451952 22.776539 8.421091 c 30 | 27.074488 12.719041 27.074488 19.687391 22.776539 23.985340 c 31 | 18.478590 28.283289 11.510241 28.283289 7.212290 23.985340 c 32 | h 33 | f* 34 | n 35 | Q 36 | 37 | endstream 38 | endobj 39 | 40 | 3 0 obj 41 | 924 42 | endobj 43 | 44 | 4 0 obj 45 | << /Annots [] 46 | /Type /Page 47 | /MediaBox [ 0.000000 0.000000 26.000000 26.000000 ] 48 | /Resources 1 0 R 49 | /Contents 2 0 R 50 | /Parent 5 0 R 51 | >> 52 | endobj 53 | 54 | 5 0 obj 55 | << /Kids [ 4 0 R ] 56 | /Count 1 57 | /Type /Pages 58 | >> 59 | endobj 60 | 61 | 6 0 obj 62 | << /Type /Catalog 63 | /Pages 5 0 R 64 | >> 65 | endobj 66 | 67 | xref 68 | 0 7 69 | 0000000000 65535 f 70 | 0000000010 00000 n 71 | 0000000034 00000 n 72 | 0000001014 00000 n 73 | 0000001036 00000 n 74 | 0000001209 00000 n 75 | 0000001283 00000 n 76 | trailer 77 | << /ID [ (some) (id) ] 78 | /Root 6 0 R 79 | /Size 7 80 | >> 81 | startxref 82 | 1342 83 | %%EOF -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/flex.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "reset.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/flex.imageset/reset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/flex.imageset/reset.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/functional-female.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5650.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/functional-female.imageset/IMG_5650.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/functional-female.imageset/IMG_5650.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/functional-male.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5651.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/functional-male.imageset/IMG_5651.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/functional-male.imageset/IMG_5651.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/must-have.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5663.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/must-have.imageset/IMG_5663.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/must-have.imageset/IMG_5663.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/raise.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5662.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/raise.imageset/IMG_5662.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/raise.imageset/IMG_5662.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/reset.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_5661.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Feed/reset.imageset/IMG_5661.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Feed/reset.imageset/IMG_5661.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/hoodie.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hoodie.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/hoodie.imageset/hoodie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Products/hoodie.imageset/hoodie.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/shorts.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shorts.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/shorts.imageset/shorts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Products/shorts.imageset/shorts.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/top.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "top.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/top.imageset/top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Products/top.imageset/top.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/tshirt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tshirt.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/Example/Resources/Assets.xcassets/Products/tshirt.imageset/tshirt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gymshark/ios-stack-kit/dc4ea6594e1477c40bca84cb17c1c48443781599/Example/Example/Resources/Assets.xcassets/Products/tshirt.imageset/tshirt.jpg -------------------------------------------------------------------------------- /Example/Example/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Example/Resources/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 09/05/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | enum Constants { 11 | 12 | enum colors { 13 | static var background = UIColor.init(white: 0.95, alpha: 1.0) 14 | static var tint = UIColor.init(red: 46.0/255.0, green: 142.0/255.0, blue: 215.0/255.0, alpha: 1.0) 15 | } 16 | 17 | enum layout { 18 | static let tableMargin: CGFloat = 10.0 19 | static let contentMargin: CGFloat = 20.0 20 | static let spacing: CGFloat = 20.0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Example/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/UI Components/Order Image Gallery/OrderGalleryImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderImage.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 19/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import ConstraintKit 11 | 12 | final class OrderGalleryImage: UIView { 13 | 14 | // MARK: - Init 15 | 16 | private let imageView = UIImageView().with({ 17 | $0.contentMode = .scaleAspectFill 18 | $0.clipsToBounds = true 19 | $0.setCornerRadius(5) 20 | }) 21 | 22 | private let label = UILabel().with({ 23 | $0.textColor = .white 24 | $0.textAlignment = .center 25 | $0.font = .systemFont(ofSize: 18, weight: .semibold) 26 | $0.backgroundColor = .init(white: 0.0, alpha: 0.8) 27 | $0.clipsToBounds = true 28 | $0.setCornerRadius(5) 29 | }) 30 | 31 | // MARK: - Init 32 | 33 | init(image: UIImage?, overlayText: String? = nil){ 34 | super.init(frame: .zero) 35 | setConstraints() 36 | imageView.image = image 37 | label.text = overlayText 38 | label.isHidden = overlayText == nil 39 | } 40 | 41 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 42 | 43 | // MARK: - Configure 44 | 45 | private func setConstraints(){ 46 | ZStack { 47 | imageView 48 | label 49 | }.withAspectRatio(0.85, priority: .init(999)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example/Example/UI Components/Order Image Gallery/OrderGalleryImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderImageGalleryView.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 19/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import SharkUtils 11 | 12 | 13 | final class OrderGalleryImageView: UIView { 14 | 15 | // MARK: - UI 16 | 17 | private let label = UILabel().with { 18 | $0.font = .systemFont(ofSize: 11.5, weight: .bold) 19 | $0.textColor = .white 20 | $0.textAlignment = .center 21 | } 22 | 23 | private let imageStack = UIStackView().with({ 24 | $0.spacing = 7 25 | $0.distribution(.fillEqually) 26 | }) 27 | 28 | // MARK: - 29 | 30 | private let imagesLimit = 4 31 | private let imagesAspect = 0.9 32 | 33 | // MARK: - Init 34 | 35 | init(){ 36 | super.init(frame: .zero) 37 | configure() 38 | setConstraints() 39 | } 40 | 41 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 42 | 43 | // MARK: - Interface 44 | 45 | var images: [String]? { 46 | didSet { 47 | imageStack.removeAllArrangedSubviews() 48 | imageStack.addArrangedSubViews(generateImages() ?? []) 49 | } 50 | } 51 | 52 | // MARK: - Configure 53 | 54 | private func configure(){ 55 | backgroundColor = .clear 56 | } 57 | 58 | private func setConstraints(){ 59 | VStack { 60 | imageStack 61 | } 62 | } 63 | 64 | // MARK: - Builders 65 | 66 | private func generateImages() -> [UIView]? { 67 | return images?.prefix(imagesLimit).enumerated().map({ (index, name) -> UIView in 68 | if index == imagesLimit - 1 { 69 | let imageCount = ((images?.count ?? 0) - index) 70 | let overlayText = (imageCount == 0) ? nil : "+\(imageCount)" 71 | 72 | return OrderGalleryImage(image: UIImage(named: name), overlayText: overlayText) 73 | 74 | } else { 75 | return OrderGalleryImage (image: UIImage(named: name)) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Example/UI Components/Tag View/TagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagView.swift 3 | // Example 4 | // 5 | // Created by Russell Warwick on 18/07/2021. 6 | // 7 | 8 | import UIKit 9 | import StackKit 10 | import SharkUtils 11 | import ConstraintKit 12 | 13 | final class TagView: UIView { 14 | 15 | // MARK: - UI 16 | 17 | private let label = UILabel().with { 18 | $0.font = .systemFont(ofSize: 11.5, weight: .bold) 19 | $0.textColor = .white 20 | $0.textAlignment = .center 21 | } 22 | 23 | // MARK: - Init 24 | 25 | init(){ 26 | super.init(frame: .zero) 27 | configure() 28 | setConstraints() 29 | } 30 | 31 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 32 | 33 | // MARK: - Interface 34 | 35 | var text: String? { 36 | didSet { 37 | label.text = text 38 | } 39 | } 40 | 41 | // MARK: - Configure 42 | 43 | private func configure(){ 44 | backgroundColor = .init(white: 0.15, alpha: 0.95) 45 | alpha = 0.95 46 | setCornerRadius(4) 47 | } 48 | 49 | private func setConstraints(){ 50 | VStack { 51 | label 52 | }.margin(.horizontal(12)) 53 | .withHeight(30) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gymshark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ConstraintKit", 6 | "repositoryURL": "https://github.com/gymshark/ios-constraint-kit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "20069bdb8db549339d12af7836913e00597aa6d8", 10 | "version": "0.0.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "StackKit", 8 | platforms: [ .iOS(.v9)], 9 | products: [ 10 | .library( 11 | name: "StackKit", 12 | targets: ["StackKit"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "ConstraintKit", 16 | url: "https://github.com/gymshark/ios-constraint-kit.git", 17 | .upToNextMinor(from: "1.0.0")) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "StackKit", 22 | dependencies: ["ConstraintKit"]), 23 | .testTarget( 24 | name: "StackKitTests", 25 | dependencies: ["StackKit"]), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | swift_standards 2 | 3 | ### Introduction 4 | 5 | StackKit harnesses the power of [UIStackView's](https://developer.apple.com/documentation/uikit/uistackview) and Swift's [ResultBuilders](https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md) together. This allows you to easily build declarative UI while only using [UIKit](https://developer.apple.com/documentation/uikit). StackKit is a great solution for anyone looking to level up their programmatic UI without having to convert an existing project to SwiftUI. Its API design is closely matched to SwiftUI making it effortless to pick up without having to accommodate any reactive programming patterns. 6 | 7 | ## Examples 8 | 9 | | Feed | Account | 10 | | :------------------------------------------------: | :--------------------------------------------: | 11 | | ![](Assets/example-1.png) | ![](Assets/example-2.png) | 12 | 13 | ## Why StackKit? 14 | 15 | The main advantage StackKit has over a standard programmatic constraints-based approach is maintenance and flexibility. You can quickly change your layout by simply moving the ordering of a couple of views. No more painful rearranging of anchors e.c.t. You can also evaluate conditionals within the UI code, adding boolean properties, loops maps, and more. 16 | 17 | We've made some Example screens and code samples would definitely recommend starting there to grasp how powerful and simplistic StackKit is. We hope to see it in your projects in the near future 🙂 18 | 19 | StackKit is built and maintained with love by the Gymshark Engineering Team 💙📱 20 | 21 | ### Why would you use StackKit instead of SwiftUI 22 | 23 | - You still want to use UIKit throughout your app 24 | - Harness the power of building UI like SwiftUI does but your team/project isn’t ready for reactive programming. 25 | - It’s easier to integrate into existing code bases. With SwiftUI you can’t swap a **View** for a **UIView**. This makes it hard to integrate old UI components into SwiftUI. 26 | - SwiftUI hides the underlying view rendering, therefore making it hard to see what is actually going on and refine issues. With StackKit you are still within UIKit so don't lose any abilities. 27 | 28 | ### Note ⚠️ 29 | 30 | If you are trying to use StackKit to handle large data sets to replace a UITableView or UICollection view you will run into performance issues. For this usecase you should use [ListKit](https://github.com/gymshark/ios-list-kit) 🦈🗒 31 | 32 | ## Installation 33 | 34 | **Swift Package Manager (SPM)** 35 | 36 | To install StackKit using [Swift Package Manager](https://github.com/apple/swift-package-manager) you can follow the [tutorial published by Apple](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) using the URL for the StackKit repo with the current version: 37 | 38 | \1. In Xcode, select “File” → “Swift Packages” → “Add Package Dependency” 39 | 40 | \2. Enter https://github.com/gymshark/ios-stack-kit.git 41 | 42 | ## Getting Started 43 | 44 | StackKit works by extending off of any `UIView`, adding a `UIStackView` to the parents' subview, setting the constraints, and then appending the views to the stack provided to it via the [ResultBuilder](https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md) This process makes laying out you UI programmatically effortless and because it is simply using [UIStackView](https://developer.apple.com/documentation/uikit/uistackview) underneath, all of the same layout logic still applies. 45 | 46 | If you haven't used UIStackView's before or need a refresher, I highly recommend checking out this [article](https://www.raywenderlich.com/2198310-uistackview-tutorial-for-ios-introducing-stack-views). Result builders are also key to the power of StackKit as well as SwiftUI. To get a better understanding of them check out this [article](https://www.swiftbysundell.com/articles/deep-dive-into-swift-function-builders/). They are awesome 🔥. 47 | 48 | With StackKit there are 4 main types of Stacks that you can use. VStack, HStack, VScroll and HScroll. Here are some common code examples below to help you get started with StackKit by extending off your UIView's. 49 | 50 | #### UIViewController 51 | 52 | --- 53 | 54 | 55 | 56 | ```swift 57 | func viewDidLoad(){ 58 | view.VStack { 59 | avatar 60 | displayName 61 | friendStatus 62 | Spacer() // Fills rest of space 63 | } 64 | } 65 | ``` 66 | 67 | --- 68 | 69 | #### UIView 70 | 71 | --- 72 | 73 | 74 | 75 | ```swift 76 | class ProfileView: UIView { 77 | init(){ 78 | HStack { 79 | avatar 80 | displayName 81 | friendStatus 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | --- 88 | 89 | ## Stack's within stack's... ([Stackception](Assets/stackwithinastack.png) 🤯) 90 | 91 | Above you can see how extending off a UIView and placing UIView items within a Stack is a relatively straightforward process. But what about putting a stack within a stack? Because our result builder is expecting type is a UIView, we can simply pass in another Stack which inturn also excepts UIViews. But, for this, we need to use a Class variant of the Stack instead of extending off of a UIView. Thankfully this is all taken care of and you don't need to do anything differently. Here is a nice example of stacks within stacks. 92 | 93 | --- 94 | 95 | 96 | 97 | ```swift 98 | class ProductsView: UIView { 99 | init(){ 100 | HStack { 101 | VStack { 102 | title1 103 | product1 104 | HStack { 105 | statusIcon1 106 | statusLabel1 107 | } 108 | orderButton1 109 | } 110 | VStack { 111 | title2 112 | product2 113 | HStack { 114 | statusIcon2 115 | statusLabel2 116 | } 117 | orderButton2 118 | } 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## Layout and customisation 125 | 126 | You might be asking but how do I control the layout, what about sizing constraints and setting spacing and padding. Don't worry we have you covered. 127 | 128 | 📝 Note: StackKit comes with a handy repo called [ConstraintsKit](https://github.com/gymshark/ios-constraint-kit). It makes writing Anchors, CGSize, UIEdgeInsets far easier with less boilerplate and some nice little extensions. 129 | 130 | ### Advanced stack layout 131 | 132 | To have greater control over how a particular Stack handles the layout of its views you can access its `alignment` and `distribution` properties by chaining off the Stack. You can find out more about how to best utilize these properties for your specific needs in these articles. 133 | 134 | • https://www.hackingwithswift.com/example-code/uikit/what-are-the-different-uistackview-distribution-types 135 | • https://www.raywenderlich.com/2198310-uistackview-tutorial-for-ios-introducing-stack-views 136 | 137 | ```swift 138 | VStack { 139 | displayName 140 | 141 | VStack { 142 | bioLabel 143 | HStack { 144 | skills.compactMap({ SkillView(skill: $0) }) 145 | }.distribution(.fillEqually) 146 | .alignment(.top) 147 | } 148 | 149 | prices.compactMap({ SkillView(skill: $0) }) 150 | Spacer() 151 | 152 | }.distribution(.fill) 153 | .alignment(.center) 154 | ``` 155 | 156 | ### Spacing 157 | 158 | At some point, you will want to provide some spacing between your items. Thankfully with UIStackViews this is simple. We can chain off the specific stack using the `.spacing` function. As well as this we can also add some unique spacing in places where required using the `Spacing` class. It's worth noting that you will want to use this class when you would like the remaining area in a Stack to be taken up by an empty view. 159 | 160 | --- 161 | 162 | 163 | 164 | ```swift 165 | VStack { 166 | displayName 167 | tagline 168 | favouriteColor 169 | Spacer(h: 10) 170 | 171 | HStack { 172 | skills.compactMap({ SkillView(skill: $0) }) 173 | }.spacing(150) 174 | 175 | Spacer(h: 20) 176 | 177 | seporator 178 | 179 | Spacer() 180 | }.spacing(5) 181 | 182 | ``` 183 | 184 | --- 185 | 186 | ### Margin 187 | 188 | Applying padding around a stack is made a lot easier with UIStackViews `layoutMargins` which takes a UIEdgeInset property. To use this with your stack you can just chain off the stack again using the `.margin()` function. This provides a great way of giving spacing around views without lots of complex layout code. 189 | 190 | --- 191 | 192 | 193 | 194 | ```swift 195 | VStack { 196 | displayName 197 | tagline 198 | favouriteColor 199 | Spacer(h: 10) 200 | HStack { 201 | skills.compactMap({ SkillView(skill: $0) }) 202 | }.spacing(5) 203 | .margin(.horizontal(10)) 204 | 205 | Spacer() 206 | }.margin(.init(top: 20, left: 10, bottom: 20, right: 10) 207 | 208 | ``` 209 | 210 | --- 211 | 212 | ### Sizing 213 | 214 | Sizing of views can still be achieved using standard NSLayout anchors width and height. Although we prefer to use some handy extensions to make this a lot easier from [ConstraintsKit](https://github.com/gymshark/ios-constraint-kit). Simply chain off any view you want to apply an explicit size to using `.with()` notation. You will notice we have width, height, and square extensions available. Because we are using stacks you don't need to worry about your other views. They will expand and contract where needed. You can even apply sizing to your stacks if you wish to. 215 | 216 | --- 217 | 218 | 219 | 220 | ```swift 221 | 222 | 223 | VStack { 224 | 225 | displayName.withHeight(120) 226 | 227 | profilePicture.withSquare(150) 228 | 229 | footer.withSize(w: 300, h: 150) 230 | 231 | }.alignment(.center) 232 | .margin(.all(20) 233 | 234 | 235 | ``` 236 | 237 | --- 238 | 239 | 240 | ## Use of result builders 241 | 242 | With the power of result builders, we can also add conditional parameters and add things such as compact maps and for loops easily. This makes writing coding features a little bit easier. Some use cases might be, Feature Toggling, Hiding Views, easily applying arrays to the stacks. 243 | 244 | --- 245 | 246 | ```swift 247 | VStack { 248 | if premiumUser { 249 | PremiumBadge(name: "Russell") 250 | } 251 | 252 | userInfo.map({ 253 | UserTag(text: $0) 254 | )} 255 | 256 | HStack { 257 | (0..10).map { (id: Int) in 258 | NumberView(number: id) 259 | } 260 | } 261 | 262 | }.alignment(.center) 263 | 264 | 265 | ``` 266 | 267 | --- 268 | 269 | 270 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Helpers/Spacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spacer.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 03/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | public final class Spacer: UIView { 11 | 12 | public static func auto() -> Spacer { 13 | let s = Spacer() 14 | s.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal) 15 | s.setContentHuggingPriority(.fittingSizeLevel, for: .vertical) 16 | s.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal) 17 | s.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical) 18 | return s 19 | } 20 | 21 | // MARK: - Init 22 | 23 | public init() { 24 | super.init(frame: .zero) 25 | } 26 | 27 | public init(square: CGFloat) { 28 | super.init(frame: .zero) 29 | translatesAutoresizingMaskIntoConstraints = false 30 | widthAnchor.constraint(equalToConstant: square).isActive = true 31 | heightAnchor.constraint(equalToConstant: square).isActive = true 32 | } 33 | 34 | public init(w: CGFloat) { 35 | super.init(frame: .zero) 36 | translatesAutoresizingMaskIntoConstraints = false 37 | widthAnchor.constraint(equalToConstant: w).isActive = true 38 | } 39 | 40 | public init(h: CGFloat) { 41 | super.init(frame: .zero) 42 | translatesAutoresizingMaskIntoConstraints = false 43 | heightAnchor.constraint(equalToConstant: h).isActive = true 44 | } 45 | 46 | required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Helpers/UIViewHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewHelpers.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 03/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIView { 11 | @discardableResult 12 | func withBackground(_ color: UIColor) -> Self { 13 | backgroundColor = color 14 | return self 15 | } 16 | } 17 | 18 | extension UIStackView { 19 | func addArrangedSubViews(_ views: [UIView]) { 20 | views.forEach { addArrangedSubview($0) } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Scroll/ScrollStacks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollStacks.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 02/05/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - UIView Extension Variants 11 | 12 | public extension UIView { 13 | 14 | @discardableResult 15 | func VScroll(useSafeArea: Bool = true, @UIViewBuilder views: () -> [UIView]) -> ScrollViewBuilder { 16 | let scrollView = ScrollViewBuilder(axis: .vertical, views: views) 17 | 18 | VStack(useSafeArea: useSafeArea) { scrollView } 19 | 20 | return scrollView 21 | } 22 | 23 | @discardableResult 24 | func HScroll(useSafeArea: Bool = true, @UIViewBuilder views: () -> [UIView]) -> ScrollViewBuilder { 25 | let scrollView = ScrollViewBuilder(axis: .horizontal, views: views) 26 | 27 | VStack(useSafeArea: useSafeArea) { scrollView } 28 | 29 | return scrollView 30 | } 31 | } 32 | 33 | // MARK: - Class Variants 34 | 35 | public class HScroll: ScrollViewBuilder { 36 | public init(@UIViewBuilder views: () -> [UIView]) { 37 | super.init(axis: .horizontal, views: views) 38 | } 39 | 40 | public required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 41 | } 42 | 43 | public class VScroll: ScrollViewBuilder { 44 | public init(@UIViewBuilder views: () -> [UIView]) { 45 | super.init(axis: .vertical, views: views) 46 | } 47 | 48 | public required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Scroll/ScrollViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewBuilder.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 04/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | open class ScrollViewBuilder: UIView { 11 | 12 | // MARK: - UI 13 | 14 | private let scrollView: UIScrollView = { 15 | let sv = UIScrollView() 16 | sv.translatesAutoresizingMaskIntoConstraints = false 17 | sv.showsHorizontalScrollIndicator = false 18 | sv.showsVerticalScrollIndicator = false 19 | return sv 20 | }() 21 | 22 | private let stackView: UIStackView = { 23 | let sv = UIStackView() 24 | sv.alignment = .fill 25 | sv.translatesAutoresizingMaskIntoConstraints = false 26 | return sv 27 | }() 28 | 29 | // MARK: - Init 30 | 31 | public init(axis: NSLayoutConstraint.Axis, @UIViewBuilder views: () -> [UIView]) { 32 | 33 | super.init(frame: .zero) 34 | 35 | addSubview(scrollView) 36 | scrollView.addSubview(stackView) 37 | 38 | if axis == .vertical { 39 | stackView.axis = .vertical 40 | stackView.distribution = .equalSpacing 41 | stackView.widthAnchor.constraint(equalTo:scrollView.widthAnchor).isActive = true 42 | 43 | } else { 44 | stackView.axis = .horizontal 45 | stackView.distribution = .fillEqually 46 | stackView.alignment = .center 47 | stackView.heightAnchor.constraint(equalTo:scrollView.heightAnchor).isActive = true 48 | } 49 | 50 | NSLayoutConstraint.activate([ 51 | scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), 52 | scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), 53 | scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), 54 | scrollView.topAnchor.constraint(equalTo:topAnchor), 55 | 56 | stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 57 | stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 58 | stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 59 | stackView.topAnchor.constraint(equalTo: scrollView.topAnchor) 60 | ]) 61 | 62 | stackView.addArrangedSubViews(views()) 63 | } 64 | 65 | required public init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 66 | } 67 | 68 | public extension ScrollViewBuilder { 69 | 70 | @discardableResult 71 | func spacing(_ spacing: CGFloat) -> Self { 72 | stackView.spacing = spacing 73 | return self 74 | } 75 | 76 | @discardableResult 77 | func margin(_ margins: UIEdgeInsets) -> Self { 78 | stackView.margin(margins) 79 | return self 80 | } 81 | 82 | @discardableResult 83 | func alignment(_ alignment: UIStackView.Alignment) -> Self { 84 | stackView.alignment = alignment 85 | return self 86 | } 87 | 88 | @discardableResult 89 | func showVerticalIndicators(_ value: Bool) -> Self { 90 | scrollView.showsVerticalScrollIndicator = value 91 | return self 92 | } 93 | 94 | @discardableResult 95 | func showHorizontalIndicators(_ value: Bool) -> Self { 96 | scrollView.showsHorizontalScrollIndicator = value 97 | return self 98 | } 99 | 100 | @discardableResult 101 | func alwaysBounceHorizontal(_ value: Bool) -> Self { 102 | scrollView.alwaysBounceHorizontal = value 103 | return self 104 | } 105 | 106 | @discardableResult 107 | func alwaysBounceVertical(_ value: Bool) -> Self { 108 | scrollView.alwaysBounceVertical = value 109 | return self 110 | } 111 | 112 | @discardableResult 113 | func scrollDelegate(_ delegate: UIScrollViewDelegate) -> Self { 114 | scrollView.delegate = delegate 115 | return self 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Stacks/StackBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StacksHelpers.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 04/07/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | @discardableResult 13 | func createStack(_ axis: NSLayoutConstraint.Axis = .vertical, 14 | views: [UIView], spacing: CGFloat = .zero, 15 | alignment: UIStackView.Alignment = .fill, 16 | distribution: UIStackView.Distribution = .fill, 17 | useSafeArea: Bool) -> UIStackView { 18 | 19 | let stackView = UIStackView(arrangedSubviews: views) 20 | stackView.axis = axis 21 | stackView.spacing = spacing 22 | stackView.alignment = alignment 23 | stackView.distribution = distribution 24 | addSubview(stackView) 25 | 26 | stackView.translatesAutoresizingMaskIntoConstraints = false 27 | 28 | if #available(iOS 11.0, *), useSafeArea { 29 | NSLayoutConstraint.activate([ 30 | stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: .zero), 31 | stackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: .zero), 32 | stackView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: .zero), 33 | stackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: .zero) 34 | ]) 35 | } else { 36 | NSLayoutConstraint.activate([ 37 | stackView.topAnchor.constraint(equalTo: topAnchor, constant: .zero), 38 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: .zero), 39 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: .zero), 40 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .zero) 41 | ]) 42 | } 43 | 44 | return stackView 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/StackKit/Stacks/Stacks/Stacks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stacks.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 01/05/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - UIView Extension Variants 11 | 12 | public extension UIView { 13 | 14 | @discardableResult 15 | func ZStack(useSafeArea: Bool = true, @UIViewBuilder views: () -> [UIView]) -> UIStackView { 16 | let container = UIView() 17 | 18 | views().forEach { view in 19 | container.VStack { view } 20 | } 21 | 22 | return VStack { container } 23 | } 24 | 25 | @discardableResult 26 | func VStack(spacing: CGFloat = .zero, useSafeArea: Bool = true, @UIViewBuilder views: () -> [UIView]) -> UIStackView { 27 | return createStack(.vertical, views: views(), spacing: spacing, alignment: .fill, distribution: .fill, useSafeArea: useSafeArea) 28 | } 29 | 30 | @discardableResult 31 | func HStack(spacing: CGFloat = .zero, useSafeArea: Bool = true, @UIViewBuilder views: () -> [UIView]) -> UIStackView { 32 | return createStack(.horizontal, views: views(), spacing: spacing, alignment: .fill, distribution: .fill, useSafeArea: useSafeArea) 33 | } 34 | } 35 | 36 | // MARK: - Class Variants 37 | 38 | public final class ZStack: UIStackView { 39 | public init(@UIViewBuilder views: () -> [UIView]) { 40 | super.init(frame: .zero) 41 | 42 | let container = UIView() 43 | 44 | views().forEach { view in 45 | container.VStack { view } 46 | } 47 | 48 | addArrangedSubview(container) 49 | } 50 | 51 | required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 52 | } 53 | 54 | public final class VStack: UIStackView { 55 | public init(spacing: CGFloat = .zero, @UIViewBuilder views: () -> [UIView]) { 56 | super.init(frame: .zero) 57 | self.axis = .vertical 58 | self.spacing = spacing 59 | self.addArrangedSubViews(views()) 60 | } 61 | 62 | required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 63 | } 64 | 65 | public final class HStack: UIStackView { 66 | public init(spacing: CGFloat = .zero, @UIViewBuilder views: () -> [UIView]) { 67 | super.init(frame: .zero) 68 | self.axis = .horizontal 69 | self.spacing = spacing 70 | self.addArrangedSubViews(views()) 71 | } 72 | 73 | required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 74 | } 75 | 76 | // MARK: - Extensions 77 | 78 | public extension UIStackView { 79 | 80 | @discardableResult 81 | func margin(_ margins: UIEdgeInsets) -> UIStackView { 82 | layoutMargins = margins 83 | isLayoutMarginsRelativeArrangement = true 84 | return self 85 | } 86 | 87 | @discardableResult 88 | func alignment(_ alignment: UIStackView.Alignment) -> UIStackView { 89 | self.alignment = alignment 90 | return self 91 | } 92 | 93 | @discardableResult 94 | func spacing(_ spacing: CGFloat) -> UIStackView { 95 | self.spacing = spacing 96 | return self 97 | } 98 | 99 | @discardableResult 100 | func distribution(_ distribution: UIStackView.Distribution) -> UIStackView { 101 | self.distribution = distribution 102 | return self 103 | } 104 | 105 | @discardableResult 106 | func withBackground(_ color: UIColor, cornerRadius: CGFloat = .zero) -> UIStackView { 107 | let view = UIView(frame: bounds) 108 | view.backgroundColor = color 109 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 110 | view.layer.cornerRadius = cornerRadius 111 | if #available(iOS 11.0, *) { 112 | view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] 113 | } 114 | insertSubview(view, at: 0) 115 | return self 116 | } 117 | 118 | @discardableResult 119 | func removeAllArrangedSubviews() -> [UIView] { 120 | return arrangedSubviews.reduce([]) { (removedSubviews, subview) -> [UIView] in 121 | removeArrangedSubview(subview) 122 | NSLayoutConstraint.deactivate(subview.constraints) 123 | subview.removeFromSuperview() 124 | return removedSubviews + [subview] 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/StackKit/UIViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewBuilder.swift 3 | // 4 | // 5 | // Created by Russell Warwick on 01/05/2021. 6 | // 7 | 8 | import UIKit 9 | 10 | @resultBuilder 11 | public struct UIViewBuilder { 12 | 13 | public typealias Expression = UIView 14 | public typealias Component = [UIView] 15 | 16 | public static func buildExpression(_ expression: Expression) -> Component { 17 | return [expression] 18 | } 19 | 20 | public static func buildExpression(_ expression: Component) -> Component { 21 | return expression 22 | } 23 | 24 | public static func buildExpression(_ expression: Expression?) -> Component { 25 | guard let expression = expression else { return [] } 26 | return [expression] 27 | } 28 | 29 | public static func buildBlock(_ children: Component...) -> Component { 30 | return children.flatMap { $0 } 31 | } 32 | 33 | public static func buildBlock(_ component: Component) -> Component { 34 | return component 35 | } 36 | 37 | public static func buildOptional(_ children: Component?) -> Component { 38 | return children ?? [] 39 | } 40 | 41 | public static func buildEither(first child: Component) -> Component { 42 | return child 43 | } 44 | 45 | public static func buildEither(second child: Component) -> Component { 46 | return child 47 | } 48 | 49 | public static func buildArray(_ components: [Component]) -> Component { 50 | return components.flatMap { $0 } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /StackKit.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint StackKit.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'StackKit' 11 | s.version = '0.1.0' 12 | s.summary = 'The power of SwiftUI with UIKit' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | StackKit harnesses the power of UIStackView's and Swift's ResultBuilders together. This allows you to easily build declarative UI while only using UIKit. StackKit is a great solution for anyone looking to level up their programmatic UI without having to convert an existing project to SwiftUI. Its API design is closely matched to SwiftUI making it effortless to pick up without having to accommodate any reactive programming patterns. 22 | DESC 23 | 24 | s.homepage = 'https://github.com/gymshark/StackKit' 25 | s.screenshots = 'https://github.com/gymshark/ios-stack-kit/raw/master/Assets/example-1.png', 'https://github.com/gymshark/ios-stack-kit/raw/master/Assets/example-2.png' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'gymshark' => 'engineering@gymshark.com' } 28 | s.source = { :git => 'https://github.com/gymshark/ios-stack-kit', :tag => s.version.to_s } 29 | # s.social_media_url = 'https://twitter.com/' 30 | 31 | s.ios.deployment_target = '9.0' 32 | 33 | s.source_files = 'Sources/StackKit/**/*' 34 | 35 | # s.resource_bundles = { 36 | # 'StackKit' => ['StackKit/Assets/*.png'] 37 | # } 38 | 39 | # s.public_header_files = 'Pod/Classes/**/*.h' 40 | # s.frameworks = 'UIKit', 'MapKit' 41 | # s.dependency 'AFNetworking', '~> 2.3' 42 | s.dependency 'ConstraintKit' 43 | end 44 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import StackKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += StackKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/StackKitTests/StackKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import StackKit 3 | 4 | final class StackKitTests: XCTestCase { 5 | func testStart(){ 6 | XCTAssert(true) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/StackKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(StackKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------