├── .configs ├── preview.yml └── project.yml ├── .gitignore ├── Dependencies ├── Package.swift ├── README.md └── Sources │ └── _PackageResources │ └── Exports.swift ├── Extensions ├── Package.swift ├── README.md └── Sources │ ├── LocalExtensions │ └── Exports.swift │ └── LocalUIExtensions │ └── Exports.swift ├── LICENSE ├── Makefile ├── Package.swift ├── Previews ├── MainFeature │ ├── Info.plist │ └── main.swift ├── Shared │ ├── AppDelegate.swift │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ └── SceneDelegate.swift └── previews.yml ├── README.md ├── Scripts ├── .core │ ├── constants.sh │ └── functions.sh ├── generate_resources.sh ├── generate_target_resources.sh ├── generate_xcodeproj.sh ├── generate_xcworkspace.sh ├── install_spmgen.sh ├── install_xcodegen.sh ├── remove_cli_tools.sh └── rename_assets.sh ├── Sources ├── AppFeature │ ├── AppViewController.swift │ └── Bootstrap │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift ├── AppUI │ └── Exports.swift ├── MainFeature │ ├── MainViewController.swift │ └── Resources │ │ └── Media.xcassets │ │ ├── .gitkeep │ │ ├── Contents.json │ │ └── usgs-unsplash-2.imageset │ │ ├── Contents.json │ │ └── usgs-hoS3dzgpHzw-unsplash-2.jpg └── Resources │ ├── Exports.swift │ └── Resources │ └── Media.xcassets │ ├── .gitkeep │ ├── Contents.json │ └── usgs-unsplash.imageset │ ├── Contents.json │ └── usgs-hoS3dzgpHzw-unsplash.jpg ├── iOS ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard └── main.swift └── project.yml /.configs/preview.yml: -------------------------------------------------------------------------------- 1 | options: 2 | bundleIdPrefix: org-domain.org-host 3 | indentWidth: 2 4 | 5 | packages: 6 | app-package: 7 | path: ./ 8 | 9 | settings: 10 | MARKETING_VERSION: "1.0.0" 11 | CURRENT_PROJECT_VERSION: "1" 12 | 13 | targetTemplates: 14 | PreviewApp: 15 | type: application 16 | platform: iOS 17 | deploymentTarget: 13.0 18 | sources: 19 | - path: ../Previews/Shared 20 | - path: ../Previews/${preview_package} 21 | dependencies: 22 | - package: app-package 23 | product: ${preview_package} 24 | preBuildScripts: 25 | - name: Generate resources boilerplate 26 | script: "\"$SRCROOT/Scripts/generate_resources.sh\"\n" 27 | info: 28 | path: ../Previews/${preview_package}/Info.plist 29 | properties: 30 | CFBundleDisplayName: ${preview_package} 31 | CFBundleShortVersionString: $(MARKETING_VERSION) 32 | CFBundleVersion: $(CURRENT_PROJECT_VERSION) 33 | UILaunchStoryboardName: LaunchScreen 34 | UIApplicationSceneManifest: 35 | UIApplicationSupportsMultipleScenes: false 36 | UISceneConfigurations: 37 | UIWindowSceneSessionRoleApplication: 38 | - UISceneConfigurationName: Default Configuration 39 | UISceneDelegateClassName: ${target_name}.SceneDelegate 40 | -------------------------------------------------------------------------------- /.configs/project.yml: -------------------------------------------------------------------------------- 1 | options: 2 | bundleIdPrefix: org-domain.org-host 3 | indentWidth: 2 4 | 5 | packages: 6 | app-package: 7 | path: ./ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ––––––––––––––––––––––––––––––––– Generated –––––––––––––––––––––––––––––––––– 2 | 3 | *.xcodeproj 4 | *.xcworkspace 5 | Sources/**/Resources.generated.swift 6 | iOS/Info.plist 7 | 8 | # –––––––––––––––––––––––––––––––– Scripts ––––––––––––––––––––––––––––––––– 9 | 10 | Scripts/.bin 11 | 12 | # ––––––––––––––––––––––––––––––– Swift Package Manager ––––––––––––––––––––––––––––––– 13 | 14 | Packages/ 15 | Package.pins 16 | Package.resolved 17 | .build/ 18 | .swiftpm 19 | 20 | # ––––––––––––––––––––––––––––– Build generated ––––––––––––––––––––––––––––– 21 | 22 | build/ 23 | DerivedData/ 24 | 25 | # ––––––––––––––––––––––––––––– Various settings –––––––––––––––––––––––––––– 26 | 27 | *.pbxuser 28 | !default.pbxuser 29 | *.mode1v3 30 | !default.mode1v3 31 | *.mode2v3 32 | !default.mode2v3 33 | *.perspectivev3 34 | !default.perspectivev3 35 | xcuserdata/ 36 | 37 | # –––––––––––––––––––––––––––––––– Fastlane ––––––––––––––––––––––––––––––––– 38 | # It is recommended to not store the screenshots in the git repo. Instead, 39 | # use fastlane to re-generate the screenshots whenever they are needed. 40 | # For more information about the recommended setup visit: 41 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 42 | 43 | fastlane/report.xml 44 | fastlane/Preview.html 45 | fastlane/screenshots/**/*.png 46 | fastlane/test_output 47 | 48 | # ––––––––––––––––––––––––––– Obj-C/Swift specific –––––––––––––––––––––––––– 49 | 50 | *.hmap 51 | *.ipa 52 | *.dSYM.zip 53 | *.dSYM 54 | 55 | # –––––––––––––––––––––––––––––––– CocoaPods –––––––––––––––––––––––––––––––– 56 | 57 | Pods/ 58 | 59 | # ––––––––––––––––––––––––––––––– Playgrounds ––––––––––––––––––––––––––––––– 60 | 61 | timeline.xctimeline 62 | playground.xcworkspace 63 | 64 | 65 | # –––––––––––––––––––––––––––––––––– Other –––––––––––––––––––––––––––––––––– 66 | 67 | *.moved-aside 68 | *.xccheckout 69 | *.xcscmblueprint 70 | .DS_Store 71 | .vscode 72 | -------------------------------------------------------------------------------- /Dependencies/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Dependencies", 7 | platforms: [ 8 | .iOS(.v13) 9 | ], 10 | dependencies: [ 11 | .package(path: "../Extensions"), 12 | .package( 13 | url: "https://github.com/capturecontext/swift-package-resources.git", 14 | .upToNextMajor(from: "2.0.0") 15 | ), 16 | ], 17 | producibleTargets: [ 18 | 19 | // MARK: P 20 | 21 | .target( 22 | name: "_PackageResources", 23 | product: .library(.static), 24 | dependencies: [ 25 | .product( 26 | name: "PackageResources", 27 | package: "swift-package-resources" 28 | ), 29 | ] 30 | ), 31 | ] 32 | ) 33 | 34 | // MARK: - Helpers 35 | 36 | enum ProductType: Equatable { 37 | case executable 38 | case library(PackageDescription.Product.Library.LibraryType? = .static) 39 | } 40 | 41 | struct ProducibleTarget { 42 | init( 43 | target: Target, 44 | productType: ProductType? = .none 45 | ) { 46 | self.target = target 47 | self.productType = productType 48 | } 49 | 50 | var target: Target 51 | var productType: ProductType? 52 | 53 | var product: PackageDescription.Product? { 54 | switch productType { 55 | case .executable: 56 | // return .executable(name: target.name, targets: [target.name]) 57 | return nil 58 | case .library(let type): 59 | return .library(name: target.name, type: type, targets: [target.name]) 60 | case .none: 61 | return nil 62 | } 63 | } 64 | 65 | static func target( 66 | name: String, 67 | product productType: ProductType? = nil, 68 | dependencies: [Target.Dependency] = [], 69 | path: String? = nil, 70 | exclude: [String] = [], 71 | sources: [String]? = nil, 72 | resources: [Resource]? = nil, 73 | publicHeadersPath: String? = nil, 74 | cSettings: [CSetting]? = nil, 75 | cxxSettings: [CXXSetting]? = nil, 76 | swiftSettings: [SwiftSetting]? = nil, 77 | linkerSettings: [LinkerSetting]? = nil 78 | ) -> ProducibleTarget { 79 | ProducibleTarget( 80 | target: productType == .executable 81 | ? .executableTarget( 82 | name: name, 83 | dependencies: dependencies, 84 | path: path, 85 | exclude: exclude, 86 | sources: sources, 87 | resources: resources, 88 | publicHeadersPath: publicHeadersPath, 89 | cSettings: cSettings, 90 | cxxSettings: cxxSettings, 91 | swiftSettings: swiftSettings, 92 | linkerSettings: linkerSettings 93 | ) 94 | : .target( 95 | name: name, 96 | dependencies: dependencies, 97 | path: path, 98 | exclude: exclude, 99 | sources: sources, 100 | resources: resources, 101 | publicHeadersPath: publicHeadersPath, 102 | cSettings: cSettings, 103 | cxxSettings: cxxSettings, 104 | swiftSettings: swiftSettings, 105 | linkerSettings: linkerSettings 106 | ), 107 | productType: productType 108 | ) 109 | } 110 | 111 | static func testTarget( 112 | name: String, 113 | dependencies: [Target.Dependency] = [], 114 | path: String? = nil, 115 | exclude: [String] = [], 116 | sources: [String]? = nil, 117 | resources: [Resource]? = nil, 118 | cSettings: [CSetting]? = nil, 119 | cxxSettings: [CXXSetting]? = nil, 120 | swiftSettings: [SwiftSetting]? = nil, 121 | linkerSettings: [LinkerSetting]? = nil 122 | ) -> ProducibleTarget { 123 | ProducibleTarget( 124 | target: .testTarget( 125 | name: name, 126 | dependencies: dependencies, 127 | path: path, 128 | exclude: exclude, 129 | sources: sources, 130 | resources: resources, 131 | cSettings: cSettings, 132 | cxxSettings: cxxSettings, 133 | swiftSettings: swiftSettings, 134 | linkerSettings: linkerSettings 135 | ), 136 | productType: .none 137 | ) 138 | } 139 | } 140 | 141 | extension Package { 142 | convenience init( 143 | name: String, 144 | defaultLocalization: LanguageTag? = nil, 145 | platforms: [SupportedPlatform]? = nil, 146 | pkgConfig: String? = nil, 147 | providers: [SystemPackageProvider]? = nil, 148 | dependencies: [Dependency] = [], 149 | producibleTargets: [ProducibleTarget], 150 | swiftLanguageVersions: [SwiftVersion]? = nil, 151 | cLanguageStandard: CLanguageStandard? = nil, 152 | cxxLanguageStandard: CXXLanguageStandard? = nil 153 | ) { 154 | self.init( 155 | name: name, 156 | defaultLocalization: defaultLocalization, 157 | platforms: platforms, 158 | pkgConfig: pkgConfig, 159 | providers: providers, 160 | products: producibleTargets.compactMap(\.product), 161 | dependencies: dependencies, 162 | targets: producibleTargets.map(\.target), 163 | swiftLanguageVersions: swiftLanguageVersions, 164 | cLanguageStandard: cLanguageStandard, 165 | cxxLanguageStandard: cxxLanguageStandard 166 | ) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Dependencies/README.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | A separate target is created for each dependency domain, dependencies can be extended in isolation in such targets. 4 | 5 | To add a new dependency: 6 | 7 | - Add a dependency to `Package.swift` (Note: Use `_`-prefixed name like `_SnapKit` for `SnapKit`) 8 | - Create a corresponding folder in [Sources](Sources) 9 | - Add `Exports.swift` file in a new target with needed exports 10 | - You can add more files to extend your dependency in isolation 11 | - You can depend on `Extensions` package when extending dependencies, just remember to include the corresponding product from `Extensions` package to your dependency target 12 | - Do not forget to specify products for your dependencies 13 | -------------------------------------------------------------------------------- /Dependencies/Sources/_PackageResources/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import PackageResources 2 | -------------------------------------------------------------------------------- /Extensions/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | // MARK: - Package 6 | 7 | let package = Package( 8 | name: "Extensions", 9 | platforms: [ 10 | .iOS(.v13) 11 | ], 12 | dependencies: [ 13 | .package( 14 | url: "https://github.com/capturecontext/swift-declarative-configuration.git", 15 | .upToNextMinor(from: "0.2.0") 16 | ), 17 | ], 18 | producibleTargets: [ 19 | .target( 20 | name: "LocalExtensions", 21 | product: .library(.static), 22 | dependencies: [ 23 | .product( 24 | name: "DeclarativeConfiguration", 25 | package: "swift-declarative-configuration" 26 | ), 27 | ] 28 | ), 29 | .target( 30 | name: "LocalUIExtensions", 31 | product: .library(.static), 32 | dependencies: [ 33 | .target(name: "LocalExtensions") 34 | ] 35 | ), 36 | ] 37 | ) 38 | 39 | // MARK: - Helpers 40 | 41 | enum ProductType: Equatable { 42 | case executable 43 | case library(PackageDescription.Product.Library.LibraryType? = .static) 44 | } 45 | 46 | struct ProducibleTarget { 47 | init( 48 | target: Target, 49 | productType: ProductType? = .none 50 | ) { 51 | self.target = target 52 | self.productType = productType 53 | } 54 | 55 | var target: Target 56 | var productType: ProductType? 57 | 58 | var product: PackageDescription.Product? { 59 | switch productType { 60 | case .executable: 61 | // return .executable(name: target.name, targets: [target.name]) 62 | return nil 63 | case .library(let type): 64 | return .library(name: target.name, type: type, targets: [target.name]) 65 | case .none: 66 | return nil 67 | } 68 | } 69 | 70 | static func target( 71 | name: String, 72 | product productType: ProductType? = nil, 73 | dependencies: [Target.Dependency] = [], 74 | path: String? = nil, 75 | exclude: [String] = [], 76 | sources: [String]? = nil, 77 | resources: [Resource]? = nil, 78 | publicHeadersPath: String? = nil, 79 | cSettings: [CSetting]? = nil, 80 | cxxSettings: [CXXSetting]? = nil, 81 | swiftSettings: [SwiftSetting]? = nil, 82 | linkerSettings: [LinkerSetting]? = nil 83 | ) -> ProducibleTarget { 84 | ProducibleTarget( 85 | target: productType == .executable 86 | ? .executableTarget( 87 | name: name, 88 | dependencies: dependencies, 89 | path: path, 90 | exclude: exclude, 91 | sources: sources, 92 | resources: resources, 93 | publicHeadersPath: publicHeadersPath, 94 | cSettings: cSettings, 95 | cxxSettings: cxxSettings, 96 | swiftSettings: swiftSettings, 97 | linkerSettings: linkerSettings 98 | ) 99 | : .target( 100 | name: name, 101 | dependencies: dependencies, 102 | path: path, 103 | exclude: exclude, 104 | sources: sources, 105 | resources: resources, 106 | publicHeadersPath: publicHeadersPath, 107 | cSettings: cSettings, 108 | cxxSettings: cxxSettings, 109 | swiftSettings: swiftSettings, 110 | linkerSettings: linkerSettings 111 | ), 112 | productType: productType 113 | ) 114 | } 115 | 116 | static func testTarget( 117 | name: String, 118 | dependencies: [Target.Dependency] = [], 119 | path: String? = nil, 120 | exclude: [String] = [], 121 | sources: [String]? = nil, 122 | resources: [Resource]? = nil, 123 | cSettings: [CSetting]? = nil, 124 | cxxSettings: [CXXSetting]? = nil, 125 | swiftSettings: [SwiftSetting]? = nil, 126 | linkerSettings: [LinkerSetting]? = nil 127 | ) -> ProducibleTarget { 128 | ProducibleTarget( 129 | target: .testTarget( 130 | name: name, 131 | dependencies: dependencies, 132 | path: path, 133 | exclude: exclude, 134 | sources: sources, 135 | resources: resources, 136 | cSettings: cSettings, 137 | cxxSettings: cxxSettings, 138 | swiftSettings: swiftSettings, 139 | linkerSettings: linkerSettings 140 | ), 141 | productType: .none 142 | ) 143 | } 144 | } 145 | 146 | extension Package { 147 | convenience init( 148 | name: String, 149 | defaultLocalization: LanguageTag? = nil, 150 | platforms: [SupportedPlatform]? = nil, 151 | pkgConfig: String? = nil, 152 | providers: [SystemPackageProvider]? = nil, 153 | dependencies: [Dependency] = [], 154 | producibleTargets: [ProducibleTarget], 155 | swiftLanguageVersions: [SwiftVersion]? = nil, 156 | cLanguageStandard: CLanguageStandard? = nil, 157 | cxxLanguageStandard: CXXLanguageStandard? = nil 158 | ) { 159 | self.init( 160 | name: name, 161 | defaultLocalization: defaultLocalization, 162 | platforms: platforms, 163 | pkgConfig: pkgConfig, 164 | providers: providers, 165 | products: producibleTargets.compactMap(\.product), 166 | dependencies: dependencies, 167 | targets: producibleTargets.map(\.target), 168 | swiftLanguageVersions: swiftLanguageVersions, 169 | cLanguageStandard: cLanguageStandard, 170 | cxxLanguageStandard: cxxLanguageStandard 171 | ) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Extensions/README.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | A package for core dependencies and extensions. 4 | 5 | Stuff implemented here should be generic enough to be needed in any module of the project, adding redundant stuff may slightly increase compile time. 6 | 7 | - LocalExtensions exports core packages and declares generic UI-independent extensions for the app 8 | - LocalUIExtensions exports generic UI components and LocalExtensions 9 | 10 | You can add more targets if needed for more specialized stuff that is complex and generic enough that you plan to extract it to a separate package and maybe open-source it. 11 | -------------------------------------------------------------------------------- /Extensions/Sources/LocalExtensions/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import DeclarativeConfiguration 2 | @_exported import Combine 3 | @_exported import Foundation 4 | -------------------------------------------------------------------------------- /Extensions/Sources/LocalUIExtensions/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import LocalExtensions 2 | @_exported import UIKit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CaptureContext (*1) 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 | 23 | 1. CaptureContext: 24 | - @maximkrouk (Maksim Kruk) 25 | - @japanese-goblinn (Kirill Gorbachyonok) 26 | - @ilevio (Ilya Yelagov) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bootstrap: 2 | @make install_xcodegen 3 | @make install_spmgen 4 | @make project 5 | @make workspace 6 | @make resources 7 | 8 | remove_cli_tools: 9 | @chmod +x ./scripts/remove_cli_tools.sh 10 | @./scripts/remove_cli_tools.sh 11 | 12 | install_spmgen: 13 | @chmod +x ./scripts/install_spmgen.sh 14 | @./scripts/install_spmgen.sh 15 | 16 | install_xcodegen: 17 | @chmod +x ./scripts/install_xcodegen.sh 18 | @./scripts/install_xcodegen.sh 19 | 20 | resources: 21 | @chmod +x ./scripts/generate_resources.sh 22 | @./scripts/generate_resources.sh 23 | 24 | workspace: 25 | @chmod +x ./scripts/generate_xcworkspace.sh 26 | @./scripts/generate_xcworkspace.sh 27 | 28 | project: 29 | @chmod +x ./scripts/generate_xcodeproj.sh 30 | @./scripts/generate_xcodeproj.sh 31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "app-package", 7 | platforms: [ 8 | .iOS(.v13) 9 | ], 10 | dependencies: [ 11 | .package(path: "./Dependencies"), 12 | .package(path: "./Extensions") 13 | ], 14 | producibleTargets: [ 15 | 16 | // MARK: - A 17 | 18 | .target( 19 | name: "AppFeature", 20 | product: .library(.static), 21 | dependencies: [ 22 | .target(name: "MainFeature") 23 | ] 24 | ), 25 | 26 | .target( 27 | name: "AppUI", 28 | product: .library(.static), 29 | dependencies: [ 30 | .localUIExtensions, 31 | .target(name: "Resources") 32 | ] 33 | ), 34 | 35 | // MARK: - M 36 | 37 | .target( 38 | name: "MainFeature", 39 | product: .library(.static), 40 | dependencies: [ 41 | .target(name: "AppUI") 42 | ] 43 | ), 44 | 45 | // MARK: - R 46 | 47 | .target( 48 | name: "Resources", 49 | product: .library(.static), 50 | dependencies: [ 51 | .dependency("_PackageResources") 52 | ], 53 | resources: [ 54 | .process("Resources") 55 | ] 56 | ), 57 | ] 58 | ) 59 | 60 | // MARK: - Helpers 61 | 62 | extension Target.Dependency { 63 | static var localUIExtensions: Target.Dependency { 64 | .product(name: "LocalUIExtensions", package: "Extensions") 65 | } 66 | 67 | static var localExtensions: Target.Dependency { 68 | .product(name: "LocalExtensions", package: "Extensions") 69 | } 70 | 71 | static func dependency(_ name: String) -> Target.Dependency { 72 | .product(name: name, package: "Dependencies") 73 | } 74 | } 75 | 76 | enum ProductType: Equatable { 77 | case executable 78 | case library(PackageDescription.Product.Library.LibraryType? = .static) 79 | } 80 | 81 | struct ProducibleTarget { 82 | init( 83 | target: Target, 84 | productType: ProductType? = .none 85 | ) { 86 | self.target = target 87 | self.productType = productType 88 | } 89 | 90 | var target: Target 91 | var productType: ProductType? 92 | 93 | var product: PackageDescription.Product? { 94 | switch productType { 95 | case .executable: 96 | // return .executable(name: target.name, targets: [target.name]) 97 | return nil 98 | case .library(let type): 99 | return .library(name: target.name, type: type, targets: [target.name]) 100 | case .none: 101 | return nil 102 | } 103 | } 104 | 105 | static func target( 106 | name: String, 107 | product productType: ProductType? = nil, 108 | dependencies: [Target.Dependency] = [], 109 | path: String? = nil, 110 | exclude: [String] = [], 111 | sources: [String]? = nil, 112 | resources: [Resource]? = nil, 113 | publicHeadersPath: String? = nil, 114 | cSettings: [CSetting]? = nil, 115 | cxxSettings: [CXXSetting]? = nil, 116 | swiftSettings: [SwiftSetting]? = nil, 117 | linkerSettings: [LinkerSetting]? = nil 118 | ) -> ProducibleTarget { 119 | ProducibleTarget( 120 | target: productType == .executable 121 | ? .executableTarget( 122 | name: name, 123 | dependencies: dependencies, 124 | path: path, 125 | exclude: exclude, 126 | sources: sources, 127 | resources: resources, 128 | publicHeadersPath: publicHeadersPath, 129 | cSettings: cSettings, 130 | cxxSettings: cxxSettings, 131 | swiftSettings: swiftSettings, 132 | linkerSettings: linkerSettings 133 | ) 134 | : .target( 135 | name: name, 136 | dependencies: dependencies, 137 | path: path, 138 | exclude: exclude, 139 | sources: sources, 140 | resources: resources, 141 | publicHeadersPath: publicHeadersPath, 142 | cSettings: cSettings, 143 | cxxSettings: cxxSettings, 144 | swiftSettings: swiftSettings, 145 | linkerSettings: linkerSettings 146 | ), 147 | productType: productType 148 | ) 149 | } 150 | 151 | static func testTarget( 152 | name: String, 153 | dependencies: [Target.Dependency] = [], 154 | path: String? = nil, 155 | exclude: [String] = [], 156 | sources: [String]? = nil, 157 | resources: [Resource]? = nil, 158 | cSettings: [CSetting]? = nil, 159 | cxxSettings: [CXXSetting]? = nil, 160 | swiftSettings: [SwiftSetting]? = nil, 161 | linkerSettings: [LinkerSetting]? = nil 162 | ) -> ProducibleTarget { 163 | ProducibleTarget( 164 | target: .testTarget( 165 | name: name, 166 | dependencies: dependencies, 167 | path: path, 168 | exclude: exclude, 169 | sources: sources, 170 | resources: resources, 171 | cSettings: cSettings, 172 | cxxSettings: cxxSettings, 173 | swiftSettings: swiftSettings, 174 | linkerSettings: linkerSettings 175 | ), 176 | productType: .none 177 | ) 178 | } 179 | } 180 | 181 | extension Package { 182 | convenience init( 183 | name: String, 184 | defaultLocalization: LanguageTag? = nil, 185 | platforms: [SupportedPlatform]? = nil, 186 | pkgConfig: String? = nil, 187 | providers: [SystemPackageProvider]? = nil, 188 | dependencies: [Dependency] = [], 189 | producibleTargets: [ProducibleTarget], 190 | swiftLanguageVersions: [SwiftVersion]? = nil, 191 | cLanguageStandard: CLanguageStandard? = nil, 192 | cxxLanguageStandard: CXXLanguageStandard? = nil 193 | ) { 194 | self.init( 195 | name: name, 196 | defaultLocalization: defaultLocalization, 197 | platforms: platforms, 198 | pkgConfig: pkgConfig, 199 | providers: providers, 200 | products: producibleTargets.compactMap(\.product), 201 | dependencies: dependencies, 202 | targets: producibleTargets.map(\.target), 203 | swiftLanguageVersions: swiftLanguageVersions, 204 | cLanguageStandard: cLanguageStandard, 205 | cxxLanguageStandard: cxxLanguageStandard 206 | ) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Previews/MainFeature/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | MainFeature 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | MainFeaturePreview.SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | 43 | 44 | -------------------------------------------------------------------------------- /Previews/MainFeature/main.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MainFeature 3 | 4 | UIApplication.shared 5 | .launchPreview(of: MainViewController()) 6 | -------------------------------------------------------------------------------- /Previews/Shared/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class AppDelegate: UIResponder, UIApplicationDelegate { 4 | public func application( 5 | _ application: UIApplication, 6 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 7 | ) -> Bool { 8 | return true 9 | } 10 | 11 | // MARK: UISceneSession Lifecycle 12 | 13 | public func application( 14 | _ application: UIApplication, 15 | configurationForConnecting connectingSceneSession: UISceneSession, 16 | options: UIScene.ConnectionOptions 17 | ) -> UISceneConfiguration { 18 | return UISceneConfiguration( 19 | name: "Default Configuration", 20 | sessionRole: connectingSceneSession.role 21 | ) 22 | } 23 | 24 | public func application( 25 | _ application: UIApplication, 26 | didDiscardSceneSessions sceneSessions: Set 27 | ) {} 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Previews/Shared/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 | -------------------------------------------------------------------------------- /Previews/Shared/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 | -------------------------------------------------------------------------------- /Previews/Shared/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Previews/Shared/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 | -------------------------------------------------------------------------------- /Previews/Shared/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIApplication { 4 | fileprivate static var initialViewController: UIViewController! 5 | 6 | public func launchPreview( 7 | of initialViewController: UIViewController, 8 | appDelegate: UIApplicationDelegate = AppDelegate() 9 | ) { 10 | UIApplication.initialViewController = initialViewController 11 | UIApplication.shared.delegate = appDelegate 12 | 13 | _ = UIApplicationMain( 14 | CommandLine.argc, 15 | CommandLine.unsafeArgv, 16 | nil, 17 | nil 18 | ) 19 | } 20 | } 21 | 22 | public class SceneDelegate: UIResponder, UIWindowSceneDelegate { 23 | public var window: UIWindow? 24 | 25 | public func scene( 26 | _ scene: UIScene, 27 | willConnectTo session: UISceneSession, 28 | options connectionOptions: UIScene.ConnectionOptions 29 | ) { 30 | guard let windowScene = scene as? UIWindowScene 31 | else { return } 32 | 33 | let window = UIWindow(windowScene: windowScene) 34 | self.window = window 35 | 36 | window.rootViewController = UIApplication.initialViewController 37 | 38 | window.makeKeyAndVisible() 39 | } 40 | 41 | public func sceneDidDisconnect(_ scene: UIScene) {} 42 | public func sceneDidBecomeActive(_ scene: UIScene) {} 43 | public func sceneWillResignActive(_ scene: UIScene) {} 44 | public func sceneWillEnterForeground(_ scene: UIScene) {} 45 | public func sceneDidEnterBackground(_ scene: UIScene) {} 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Previews/previews.yml: -------------------------------------------------------------------------------- 1 | name: Previews 2 | 3 | include: 4 | - ../.configs/preview.yml 5 | 6 | targets: 7 | MainFeaturePreview: 8 | templates: 9 | - PreviewApp 10 | templateAttributes: 11 | preview_package: MainFeature 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # basic-ios-template 2 | 3 | [![SwiftPM 5.6](https://img.shields.io/badge/swiftpm-5.6-ED523F.svg?style=flat)](https://swift.org/download/) [![@maximkrouk](https://img.shields.io/badge/contact-@capture__context-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) 4 | 5 | ### Getting started 6 | 7 | 1. Fork the repo as a template. 8 | 9 | 2. Create a local folder for your app and navigate to it 10 | 11 | ```bash 12 | mkdir 13 | cd 14 | ``` 15 | 16 | 2. Clone the template, rename cloned folder to `App` and navigate to it 17 | 18 | ```bash 19 | git clone https://github.com//-ios.git 20 | mv -ios App 21 | cd App 22 | ``` 23 | 24 | > You can choose any name or avoid nesting, but we recommend to follow the example (including the case) to get the best result 😌 25 | 26 | 3. Rename [project.yml](project.yml), [.config/project.yml](.config/project.yml) and [.config/preview.yml](.config/preview.yml) contents accordingly to your needs 27 | 28 | - bundleIdPrefix: `org-domain.org-host` to your bundleID prefix 29 | - targets: `MyTarget` to `-ios` 30 | - info.properties.CFBundleDisplayName: `MyApp` to `` 31 | 32 | 4. Bootstrap the environment 33 | 34 | ```bash 35 | make bootstrap 36 | ``` 37 | 38 | > See Makefile for details 39 | 40 | Than you can commit changes and you are ready for the actual development 😎 41 | 42 | ```bash 43 | open Package.xcworkspace 44 | ``` 45 | 46 | 47 | ### Structure 48 | 49 | See [Extensions](Extensions/README.md) and [Dependencies](Dependencies/README.md) for more details for these modules 50 | 51 | Main work is happenning in the root package. 52 | 53 | - `<#Module#>Feature` naming is used for modules user directly interact with 54 | - `<#Service#>` naming modules is used for modules that are used by developers to build feature modules 55 | 56 | Basically your `Sources` folder structure will look kinda like this 57 | 58 | ```swift 59 | Sources { // Main modules 60 | AppFeature // Entry point for the app, contains AppDelegate, RootViewController, AppState etc., coordinates app flows 61 | MainFeature // Main app flow, non-main flows may be Onboarding/Admin/Auth for example. 62 | <#SomeFeature#>Feature // Any other feature 63 | AppUI // App-specific UI components 64 | APIClient // Service module example 65 | Resources // Contains shared resources and generated boilerplate, but you can declare target-specific resources too, see https://github.com/capturecontext/spmgen 66 | } 67 | ``` 68 | 69 | 70 | 71 | > **Note:** 72 | > 73 | > _Scripts can be improved later so we advice you to keep an eye on the repo and a tracking reference to our `main` branch to keep your infrastructure up to date_ 🚀 74 | 75 | 76 | 77 | ### Recommended dependencies 78 | 79 | - https://github.com/pointfreeco/swift-composable-architecture 80 | - https://github.com/pointfreeco/swift-identified-collections 81 | - https://github.com/pointfreeco/swift-parsing 82 | - https://github.com/capturecontext/swift-declarative-configuration 83 | - https://github.com/capturecontext/swift-composable-environment 84 | - https://github.com/capturecontext/swift-standard-clients 85 | - https://github.com/capturecontext/swift-capture 86 | - https://github.com/capturecontext/spmgen 87 | - https://github.com/snapkit/snapkit 88 | 89 | > Will be recommended later (yet in alpha or beta) 90 | > - https://github.com/capturecontext/composable-architecture-extensions 91 | > - https://github.com/capturecontext/swift-foundation-extensions 92 | > - https://github.com/capturecontext/swift-cocoa-extensions 93 | > - https://github.com/capturecontext/combine-extensions 94 | > - https://github.com/capturecontext/combine-cocoa 95 | > - https://github.com/capturecontext/combine-cocoa-navigation 96 | > - https://github.com/capturecontext/swift-prelude 97 | > - https://github.com/capturecontext/swift-generic-color 98 | > - https://github.com/capturecontext/swift-palette 99 | -------------------------------------------------------------------------------- /Scripts/.core/constants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ––––––––––––––––––––––––––– ASSERTIONS ––––––––––––––––––––––––– 4 | 5 | if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then 6 | echo -e "\n❌ ${RED}This script can only be used for sourcing${RESET}" 7 | exit $ERROR_CODE 8 | fi 9 | 10 | # ––––––––––––––––––––––––––– CONSTANTS ––––––––––––––––––––––––– 11 | 12 | TOOLS_INSTALL_PATH="$( cd "$(dirname "$0")" && pwd )/.bin" 13 | INSTALLERS_PATH="${TOOLS_INSTALL_PATH}/.installers" 14 | -------------------------------------------------------------------------------- /Scripts/.core/functions.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | set -e 4 | 5 | # –––––––––––––––––––––––– ENABLE TO DEBUG –––––––––––––––––––––––– 6 | 7 | # set -x 8 | 9 | # –––––––––––––––––––––––––– DECLARATIONS ––––––––––––––––––––––––– 10 | 11 | RED="\033[31m" 12 | GREEN="\033[32m" 13 | YELLOW="\033[33m" 14 | BOLD="\033[1m" 15 | PURPLE="\033[95m" 16 | RESET="\033[0m" 17 | 18 | ERROR_CODE=1 19 | SUCCESS_CODE=0 20 | 21 | # ––––––––––––––––––––––––––– ASSERTIONS ––––––––––––––––––––––––– 22 | 23 | if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then 24 | echo -e "\n❌ ${RED}This script can only be used for sourcing${RESET}" 25 | exit ${ERROR_CODE} 26 | fi 27 | 28 | # ––––––––––––––––––––––––––– FUNCTIONS ––––––––––––––––––––––––––– 29 | 30 | function print() { 31 | if [ -z "$2" ]; then 32 | local text="$1" 33 | echo -e "\n${text}\n" 34 | return $SUCCESS_CODE 35 | fi 36 | local emoji="$1" 37 | local text="$2" 38 | echo -e "\n${emoji} ${text}\n" 39 | } 40 | 41 | function print_info() { 42 | echo -e "\nℹ️ ${BOLD}${PURPLE}${1}${RESET}\n" 43 | } 44 | 45 | function print_success() { 46 | echo -e "\n✅ ${BOLD}${GREEN}${1}${RESET}\n" 47 | } 48 | 49 | function print_warning() { 50 | echo -e "\n⚠️ ${BOLD}${YELLOW}${1}${RESET}\n" 51 | } 52 | 53 | function print_error() { 54 | echo -e "\n❌ ${BOLD}${RED}${1}${RESET}\n" 55 | } 56 | 57 | function _print_info() { 58 | echo -e "ℹ️ ${BOLD}${PURPLE}${1}${RESET}" 59 | } 60 | 61 | function _print_success() { 62 | echo -e "✅ ${BOLD}${GREEN}${1}${RESET}" 63 | } 64 | 65 | function _print_warning() { 66 | echo -e "⚠️ ${BOLD}${YELLOW}${1}${RESET}" 67 | } 68 | 69 | function _print_error() { 70 | echo -e "❌ ${BOLD}${RED}${1}${RESET}" 71 | } 72 | 73 | function is_installed() { 74 | [ `command -v "$1"` 2>/dev/null ] && echo true || echo false 75 | } 76 | 77 | function install_brew_if_needed() { 78 | if $( is_installed "brew" ); then return 0; fi 79 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 80 | } 81 | 82 | function build_swift_product() { 83 | local product_name="$1" 84 | if [ -z "$product_name" ]; then 85 | print_error "PRODUCT NAME SHOULD BE PASSED" 86 | return $ERROR_CODE 87 | fi 88 | swift build --product=$product_name -c release --disable-sandbox --build-path '.build' 89 | } 90 | 91 | function force_cd() { 92 | if [ -z "$1" ]; then 93 | print_error "DIRECTORY NAME SHOULD BE PASSED" 94 | return $ERROR_CODE 95 | fi 96 | local directory="$1" 97 | cd $directory 2>/dev/null || mkdir -p $directory && cd $directory 98 | } 99 | 100 | function mkdir_if_needed() { 101 | if [ -z "$1" ]; then 102 | print_error "DIRECTORY NAME SHOULD BE PASSED" 103 | return $ERROR_CODE 104 | fi 105 | local directory="$1" 106 | if [ -d "$directory" ]; then return $SUCCESS_CODE; fi 107 | mkdir "$directory" 108 | } 109 | -------------------------------------------------------------------------------- /Scripts/generate_resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTS 4 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 5 | source "${SCRIPT_DIR_PATH}/.core/functions.sh" 6 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 7 | 8 | # FUNCTIONS 9 | function generate_resources() { 10 | local target_folder_name="$1" 11 | if [ -z "$target_folder_name" ]; then 12 | print_error "PRODUCT NAME SHOULD BE PASSED" 13 | return $ERROR_CODE 14 | fi 15 | ${SCRIPT_DIR_PATH}/generate_target_resources.sh ${target_folder_name} 16 | } 17 | 18 | # RESOURCES GENERATION 19 | generate_resources "Resources" 20 | generate_resources "MainFeature" 21 | -------------------------------------------------------------------------------- /Scripts/generate_target_resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTS 4 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 5 | source "${SCRIPT_DIR_PATH}/.core/functions.sh" 6 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 7 | 8 | # CONSTANTS 9 | TOOL="${TOOLS_INSTALL_PATH}/spmgen" 10 | 11 | TARGET_FOLDER_NAME=$1 12 | 13 | # ––––––––––––––––––––––––––– SCRIPT ––––––––––––––––––––––––––– 14 | 15 | if ! $( is_installed "${TOOL}" ); then "${SCRIPT_DIR_PATH}/install_spmgen.sh"; fi 16 | 17 | "$TOOL" resources "${SCRIPT_DIR_PATH}/../Sources/${TARGET_FOLDER_NAME}/Resources" \ 18 | --output "${SCRIPT_DIR_PATH}/../Sources/${TARGET_FOLDER_NAME}/Resources.generated.swift" \ 19 | --indentor " " \ 20 | --tab-size 2 21 | 22 | echo "" 23 | _print_success "Did generate resources for Sources/${TARGET_FOLDER_NAME}" 24 | -------------------------------------------------------------------------------- /Scripts/generate_xcodeproj.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTS 4 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 5 | source "${SCRIPT_DIR_PATH}/.core/functions.sh" 6 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 7 | 8 | # CONSTANTS 9 | TOOL="xcodegen" 10 | 11 | # ––––––––––––––––––––––––––– SCRIPT ––––––––––––––––––––––––––– 12 | 13 | if ! $( is_installed "${TOOL}" ); then "${SCRIPT_DIR_PATH}/install_xcodegen.sh"; fi 14 | 15 | "$TOOL" generate 16 | 17 | print_success "Did generate xcoderoj" 18 | -------------------------------------------------------------------------------- /Scripts/generate_xcworkspace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 4 | 5 | cd "${SCRIPT_DIR_PATH}/.." 6 | 7 | WORKSPACE="Project.xcworkspace" 8 | PROJECT="Project.xcodeproj" 9 | 10 | DEPENDENCIES="Dependencies" 11 | EXTENSIONS="Extensions" 12 | 13 | rm -rf "${WORKSPACE}" 14 | mkdir -p "${WORKSPACE}" 15 | 16 | cat > "${WORKSPACE}/contents.xcworkspacedata" < 18 | 20 | 22 | 23 | 25 | 26 | 28 | 29 | 31 | 32 | 33 | EOL 34 | -------------------------------------------------------------------------------- /Scripts/install_spmgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TODO: Support aliasing for local installations 4 | 5 | # IMPORTS 6 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 7 | source "${SCRIPT_DIR_PATH}/.core/functions.sh" 8 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 9 | 10 | # CONFIG 11 | TOOL_NAME="spmgen" 12 | TOOL_OWNER="capturecontext" 13 | TOOL_VERSION="2.1.1" 14 | 15 | # CONSTANTS 16 | TOOL_INSTALL_PATH="${TOOLS_INSTALL_PATH}/${TOOL_NAME}-tmp" 17 | TOOL_DOWNLOAD_DIR="${TOOLS_INSTALL_PATH}/${TOOL_NAME}-installer" 18 | 19 | # CLEAN UP 20 | trap clean_up err exit SIGTERM SIGINT 21 | clean_up() { 22 | rm -rf "${TOOL_INSTALL_PATH}" 23 | if [ -d "${TOOL_DOWNLOAD_DIR}" ]; then rm -rf "${TOOL_DOWNLOAD_DIR}"; fi 24 | } 25 | 26 | # ––––––––––––––––––––––––––– SCRIPT ––––––––––––––––––––––––––– 27 | 28 | if $( is_installed "${TOOLS_INSTALL_PATH}/${TOOL_NAME}" ); then 29 | print_warning "${TOOL_NAME} already installed" 30 | exit ${SUCCESS_CODE} 31 | fi 32 | 33 | force_cd "${TOOL_DOWNLOAD_DIR}" 34 | 35 | print ⬇️ "Fetching ${TOOL_NAME}..." 36 | git clone "https://github.com/${TOOL_OWNER}/${TOOL_NAME}.git" 37 | cd "${TOOL_NAME}" 38 | 39 | print 🔧 "Switching to specified version..." 40 | git fetch --all --tags 41 | git checkout tags/${TOOL_VERSION} -b local 42 | 43 | print 🔨 "Building ${TOOL_NAME}..." 44 | build_swift_product "${TOOL_NAME}" 45 | 46 | print ♻️ "Installing ${TOOL_NAME}..." 47 | mkdir_if_needed "${TOOL_INSTALL_PATH}" 48 | install "./.build/release/${TOOL_NAME}" "${TOOL_INSTALL_PATH}" 49 | rm -rf "${TOOL_DOWNLOAD_DIR}" 50 | mv "${TOOL_INSTALL_PATH}/${TOOL_NAME}" "${TOOLS_INSTALL_PATH}" 51 | 52 | print_success "${TOOL_NAME} successfully installed" 53 | -------------------------------------------------------------------------------- /Scripts/install_xcodegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Temporary xcodegen is installed globally 4 | # TODO: Implement local installation 5 | 6 | # IMPORTS 7 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 8 | source "${SCRIPT_DIR_PATH}/.core/functions.sh" 9 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 10 | 11 | # CONFIG 12 | TOOL_NAME="xcodegen" 13 | TOOL_OWNER="yonaskolb" 14 | 15 | # ––––––––––––––––––––––––––– SCRIPT ––––––––––––––––––––––––––– 16 | 17 | if $( is_installed xcodegen ); then 18 | print_warning "${TOOL_NAME} already installed" 19 | exit ${SUCCESS_CODE} 20 | fi 21 | 22 | brew install ${TOOL_NAME} 23 | 24 | print_success "${TOOL_NAME} successfully installed" -------------------------------------------------------------------------------- /Scripts/remove_cli_tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTS 4 | SCRIPT_DIR_PATH="$( cd "$(dirname "$0")" && pwd )" 5 | source "${SCRIPT_DIR_PATH}/.core/constants.sh" 6 | 7 | # ––––––––––––––––––––––––––– SCRIPT ––––––––––––––––––––––––––– 8 | 9 | rm -rf "${TOOLS_INSTALL_PATH}" 10 | -------------------------------------------------------------------------------- /Scripts/rename_assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ⚠️ TODO: REFACTORING NEEDED DUE TO .core/ 4 | 5 | # Usage 6 | # rename_assets path/to/Assets.xcassets (just rename_assets will use default path) 7 | 8 | # In current implementation do not updates `Contents.json` file :c 9 | 10 | # Possible problems: 11 | # assets is processing in order that they presented in directory and do not compared by size 12 | # so if asset, asset@2x, asset@3x is presetned in directory they should be placed in right order 13 | 14 | set -e 15 | 16 | GREEN="\033[32m" 17 | PURPLE="\033[95m" 18 | RED="\033[31m" 19 | COLOR_RESET="\033[0m" 20 | 21 | BASE_DIR=${1-"$(dirname "$0")/../Sources/Resources/Resources/Media.xcassets/"} 22 | EXT_TO_BE_RENAMED=("png" "svg" "pdf") 23 | 24 | rename_file() { 25 | if [[ ! -f $1 ]]; then return 1; fi 26 | local dir=$(dirname "$1") 27 | if [[ "${dir##*.}" != "imageset" ]]; then return 1; fi 28 | local ext="${1##*.}" 29 | if [[ ! "${EXT_TO_BE_RENAMED[@]}" =~ $ext ]]; then return 1; fi 30 | 31 | local dir=$(dirname "$1") 32 | local ext="${1##*.}" 33 | local file_to_rename="$1" 34 | local rename_to=$(basename "${dir%.*}") 35 | local dir_full="$dir" 36 | local asset_count="$2" 37 | (( asset_count += 1 )) 38 | 39 | if [[ "$asset_count" == 1 ]]; then 40 | mv "$file_to_rename" "${dir_full}/${rename_to}.${ext}" 41 | else 42 | mv "$file_to_rename" "${dir_full}/${rename_to}@${asset_count}x.${ext}" 43 | fi 44 | } 45 | 46 | traverse_files_from_dir() { 47 | local asset_count=0 48 | for i in "$1"/*; do 49 | if [[ ! -d "$i" ]]; then 50 | rename_file "$i" "$asset_count" 51 | local is_rename_succeeded=$? 52 | if [[ "$is_rename_succeeded" != 0 ]]; then continue; fi 53 | (( asset_count += 1 )) 54 | else 55 | traverse_files_from_dir "$i" 56 | fi 57 | done 58 | } 59 | 60 | if [[ $(basename "$BASE_DIR") != "Assets.xcassets" ]]; then 61 | echo -e "${RED}🛑 ERROR:${COLOR_RESET} Not an '*.xcassets' directory" 62 | exit 63 | fi 64 | 65 | echo -e "${PURPLE}🚀 Processing...${COLOR_RESET}" 66 | traverse_files_from_dir "$BASE_DIR" && echo -e "${GREEN}✅ DONE${COLOR_RESET}" 67 | -------------------------------------------------------------------------------- /Sources/AppFeature/AppViewController.swift: -------------------------------------------------------------------------------- 1 | import AppUI 2 | import MainFeature 3 | 4 | public final class AppViewController: UIViewController { 5 | public override func viewDidLoad() { 6 | super.viewDidLoad() 7 | 8 | let mainController = MainViewController() 9 | addChild(mainController) 10 | view.addSubview(mainController.view) 11 | mainController.view.frame = view.bounds 12 | mainController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 13 | mainController.didMove(toParent: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AppFeature/Bootstrap/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppUI 2 | 3 | public class AppDelegate: UIResponder, UIApplicationDelegate { 4 | public func application( 5 | _ application: UIApplication, 6 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 7 | ) -> Bool { 8 | return true 9 | } 10 | 11 | // MARK: UISceneSession Lifecycle 12 | 13 | public func application( 14 | _ application: UIApplication, 15 | configurationForConnecting connectingSceneSession: UISceneSession, 16 | options: UIScene.ConnectionOptions 17 | ) -> UISceneConfiguration { 18 | return UISceneConfiguration( 19 | name: "Default Configuration", 20 | sessionRole: connectingSceneSession.role 21 | ) 22 | } 23 | 24 | public func application( 25 | _ application: UIApplication, 26 | didDiscardSceneSessions sceneSessions: Set 27 | ) {} 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AppFeature/Bootstrap/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import AppUI 2 | 3 | public class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | public var window: UIWindow? 5 | 6 | public func scene( 7 | _ scene: UIScene, 8 | willConnectTo session: UISceneSession, 9 | options connectionOptions: UIScene.ConnectionOptions 10 | ) { 11 | guard let windowScene = scene as? UIWindowScene 12 | else { return } 13 | 14 | let window = UIWindow(windowScene: windowScene) 15 | self.window = window 16 | 17 | window.rootViewController = AppViewController() 18 | 19 | window.makeKeyAndVisible() 20 | } 21 | 22 | public func sceneDidDisconnect(_ scene: UIScene) {} 23 | public func sceneDidBecomeActive(_ scene: UIScene) {} 24 | public func sceneWillResignActive(_ scene: UIScene) {} 25 | public func sceneWillEnterForeground(_ scene: UIScene) {} 26 | public func sceneDidEnterBackground(_ scene: UIScene) {} 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Sources/AppUI/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import LocalUIExtensions 2 | @_exported import Resources -------------------------------------------------------------------------------- /Sources/MainFeature/MainViewController.swift: -------------------------------------------------------------------------------- 1 | import AppUI 2 | import SwiftUI 3 | 4 | public final class MainViewController: UITabBarController { 5 | public override func viewDidLoad() { 6 | super.viewDidLoad() 7 | 8 | tabBar.backgroundColor = .systemBackground.withAlphaComponent(0.7) 9 | setViewControllers( 10 | [ 11 | UIHostingController( 12 | rootView: VStack { 13 | HStack { 14 | Image.resource(.usgsUnsplash) 15 | .resizable() 16 | .aspectRatio(1, contentMode: .fill) 17 | .frame(width: 120, height: 120) 18 | .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) 19 | .overlay(Color.black.opacity(0.1)) 20 | .overlay( 21 | Text("shared\nresource") 22 | .foregroundColor(.white) 23 | .multilineTextAlignment(.center) 24 | .font(.system(size: 12, weight: .regular, design: .monospaced)) 25 | ) 26 | Image.resource(.usgsUnsplash_2) 27 | .resizable() 28 | .aspectRatio(1, contentMode: .fill) 29 | .frame(width: 120, height: 120) 30 | .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) 31 | .overlay(Color.black.opacity(0.1)) 32 | .overlay( 33 | Text("local\nresource") 34 | .foregroundColor(.white) 35 | .multilineTextAlignment(.center) 36 | .font(.system(size: 12, weight: .regular, design: .monospaced)) 37 | ) 38 | } 39 | Text("First") 40 | .fontWeight(.semibold) 41 | .foregroundColor(Color.white) 42 | .padding(.vertical, 6) 43 | .padding(.horizontal, 32) 44 | .background(Color.black) 45 | .clipShape(Capsule()) 46 | } 47 | .frame(maxWidth: .infinity, maxHeight: .infinity) 48 | .background( 49 | LinearGradient( 50 | colors: [ 51 | .orange, 52 | .red 53 | ], 54 | startPoint: .top, 55 | endPoint: .bottom 56 | ) 57 | .overlay(Color.black.opacity(0.1)) 58 | .edgesIgnoringSafeArea(.all) 59 | ) 60 | ).configured { $0 61 | .tabBarItem(UITabBarItem( 62 | title: "First", 63 | image: UIImage(systemName: "star"), 64 | selectedImage: UIImage(systemName: "star.fill") 65 | )) 66 | }, 67 | UIHostingController( 68 | rootView: Text("Second") 69 | .fontWeight(.semibold) 70 | .foregroundColor(Color.black) 71 | .frame(maxWidth: .infinity, maxHeight: .infinity) 72 | .background( 73 | LinearGradient( 74 | colors: [ 75 | .orange, 76 | .red 77 | ], 78 | startPoint: .top, 79 | endPoint: .bottom 80 | ) 81 | .edgesIgnoringSafeArea(.all) 82 | ) 83 | ).configured { $0 84 | .tabBarItem(UITabBarItem( 85 | title: "Second", 86 | image: UIImage(systemName: "star"), 87 | selectedImage: UIImage(systemName: "star.fill") 88 | )) 89 | }, 90 | ], 91 | animated: false 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/MainFeature/Resources/Media.xcassets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureContext/basic-ios-template/5b9faef370d67995d2337895537a1c8bafb42cf3/Sources/MainFeature/Resources/Media.xcassets/.gitkeep -------------------------------------------------------------------------------- /Sources/MainFeature/Resources/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/MainFeature/Resources/Media.xcassets/usgs-unsplash-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "usgs-hoS3dzgpHzw-unsplash-2.jpg", 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 | -------------------------------------------------------------------------------- /Sources/MainFeature/Resources/Media.xcassets/usgs-unsplash-2.imageset/usgs-hoS3dzgpHzw-unsplash-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureContext/basic-ios-template/5b9faef370d67995d2337895537a1c8bafb42cf3/Sources/MainFeature/Resources/Media.xcassets/usgs-unsplash-2.imageset/usgs-hoS3dzgpHzw-unsplash-2.jpg -------------------------------------------------------------------------------- /Sources/Resources/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import _PackageResources -------------------------------------------------------------------------------- /Sources/Resources/Resources/Media.xcassets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureContext/basic-ios-template/5b9faef370d67995d2337895537a1c8bafb42cf3/Sources/Resources/Resources/Media.xcassets/.gitkeep -------------------------------------------------------------------------------- /Sources/Resources/Resources/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Resources/Resources/Media.xcassets/usgs-unsplash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "usgs-hoS3dzgpHzw-unsplash.jpg", 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 | -------------------------------------------------------------------------------- /Sources/Resources/Resources/Media.xcassets/usgs-unsplash.imageset/usgs-hoS3dzgpHzw-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptureContext/basic-ios-template/5b9faef370d67995d2337895537a1c8bafb42cf3/Sources/Resources/Resources/Media.xcassets/usgs-unsplash.imageset/usgs-hoS3dzgpHzw-unsplash.jpg -------------------------------------------------------------------------------- /iOS/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 | -------------------------------------------------------------------------------- /iOS/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" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/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 | -------------------------------------------------------------------------------- /iOS/main.swift: -------------------------------------------------------------------------------- 1 | import AppFeature 2 | import AppUI 3 | import UIKit 4 | 5 | let delegate = AppDelegate() 6 | UIApplication.shared.delegate = delegate 7 | 8 | _ = UIApplicationMain( 9 | CommandLine.argc, 10 | CommandLine.unsafeArgv, 11 | nil, 12 | nil 13 | ) 14 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: Project 2 | 3 | include: 4 | - .configs/project.yml 5 | - Previews/previews.yml 6 | 7 | settings: 8 | MARKETING_VERSION: "1.0.0" 9 | CURRENT_PROJECT_VERSION: "1" 10 | 11 | targets: 12 | MyTarget: 13 | type: application 14 | platform: iOS 15 | deploymentTarget: 13.0 16 | sources: 17 | - path: iOS 18 | dependencies: 19 | - package: app-package 20 | product: AppFeature 21 | preBuildScripts: 22 | - name: Generate resources boilerplate 23 | script: "\"$SRCROOT/Scripts/generate_resources.sh\"\n" 24 | info: 25 | path: iOS/Info.plist 26 | properties: 27 | CFBundleDisplayName: MyApp 28 | CFBundleShortVersionString: $(MARKETING_VERSION) 29 | CFBundleVersion: $(CURRENT_PROJECT_VERSION) 30 | UILaunchStoryboardName: LaunchScreen 31 | UIApplicationSceneManifest: 32 | UIApplicationSupportsMultipleScenes: false 33 | UISceneConfigurations: 34 | UIWindowSceneSessionRoleApplication: 35 | - UISceneConfigurationName: Default Configuration 36 | UISceneDelegateClassName: AppFeature.SceneDelegate 37 | --------------------------------------------------------------------------------