├── .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 | [](https://swift.org/download/) [](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 |
--------------------------------------------------------------------------------