├── .gitignore ├── LICENSE ├── README.md ├── cookiecutter.json ├── hooks ├── post_gen_project.sh └── pre_gen_project.sh └── {{cookiecutter.projectDirectory}} ├── .gitignore ├── .swiftlint.yml ├── Modules ├── Core │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ ├── Assets │ │ │ ├── Colors.swift │ │ │ ├── Images.swift │ │ │ ├── Resources │ │ │ │ ├── Colors.xcassets │ │ │ │ │ ├── Branding │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── brandPrimary.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── Feedback │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── error.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── success.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── warning.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Images.xcassets │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── icons │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── illustrations │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── logos │ │ │ │ │ │ └── Contents.json │ │ │ │ └── de.lproj │ │ │ │ │ └── Localizable.strings │ │ │ └── Strings.swift │ │ ├── CommonUI │ │ │ ├── Extensions │ │ │ │ └── Storyboard.swift │ │ │ ├── Style │ │ │ │ └── Appearance.swift │ │ │ └── ViewState.swift │ │ ├── Models │ │ │ └── Item.swift │ │ ├── Networking │ │ │ ├── API.swift │ │ │ ├── AuthHandler.swift │ │ │ ├── CredentialsController.swift │ │ │ └── Stubs │ │ │ │ └── items.json │ │ └── Utilities │ │ │ ├── Config.swift │ │ │ ├── Decoders.swift │ │ │ ├── Environment.swift │ │ │ ├── FormatStyles.swift │ │ │ └── Logging.swift │ └── Tests │ │ └── CoreTests.swift └── Features │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources │ ├── AuthFeature │ │ ├── AuthCoordinator.swift │ │ └── LoginViewController.swift │ ├── ExampleFeature │ │ ├── ExampleCoordinator.swift │ │ ├── ExampleScreen.swift │ │ └── ExampleViewModel.swift │ └── MainFeature │ │ └── MainCoordinator.swift │ └── Tests │ └── FeaturesTests.swift ├── README.md ├── bitrise.yml ├── buildStrings ├── default.swiftformat ├── project.yml ├── swiftgen.yml ├── templates └── assets.stencil ├── texterify.json └── {{cookiecutter.projectName}} ├── Assets └── Assets.xcassets │ ├── AppIcon.appiconset │ ├── 02 orange.png │ └── Contents.json │ └── Contents.json ├── Code ├── AppCoordinator.swift ├── AppDelegate.swift └── SceneDelegate.swift └── SupportingFiles ├── Base.lproj └── LaunchScreen.storyboard ├── Configurations ├── Dev.xcconfig ├── Live.xcconfig └── Staging.xcconfig ├── Info.plist └── Settings.bundle ├── Root.plist ├── com.mono0926.LicensePlist.latest_result.txt ├── com.mono0926.LicensePlist.plist └── com.mono0926.LicensePlist ├── Alamofire.plist ├── AlamofireImage.plist ├── DataSource.plist ├── Differ.plist ├── Fetch.plist ├── KeychainAccess.plist ├── Logbook.plist ├── StatefulViewController.plist └── Toolbox.plist /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ## Build generated 4 | #build/ 5 | DerivedData 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | 18 | ## Other 19 | *.xccheckout 20 | *.moved-aside 21 | *.xcuserstate 22 | *.xcscmblueprint 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | # Carthage/Build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 aaa - all about apps GmbH 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Starter 📱 2 | 3 | Xcode 15.x with Swift Package Manager dependencies. 4 | 5 | `cookiecutter gh:allaboutapps/ios-starter` 6 | 7 | ## Installation 8 | 9 | Install [Cookiecutter](https://cookiecutter.readthedocs.io/en/latest/installation.html), [XcodeGen](https://github.com/yonaskolb/XcodeGen#installing) and [SwiftGen](https://github.com/SwiftGen/SwiftGen#installation). 10 | 11 | ``` 12 | brew install cookiecutter 13 | brew install xcodegen 14 | brew install swiftgen 15 | ``` 16 | 17 | #### Texterify Setup 18 | 19 | [Texterify](https://github.com/chrztoph/texterify) is an open source localization management system, which can be hosted on your own server or run locally. 20 | To integrate Texterify in your project, you need to install the [Texterify CLI](https://github.com/chrztoph/texterify-cli): 21 | 22 | ``` 23 | npm install -g texterify 24 | ``` 25 | 26 | Follow the configuration steps described in the [documentation](https://github.com/chrztoph/texterify-cli#configuration). 27 | 28 | ## Steps 29 | 30 | 1. Run `cookiecutter gh:allaboutapps/ios-starter`. 31 | 2. You'll be asked for project name, team details and bundle identifier details. If you don't have the localization tool installed, skip the `texterify` parameters. `cookiecutter` will create all files needed from the template on `github`. 32 | 3. `xcodegen` will run automatically and generate the `Xcode` project file. 33 | 4. Xcode launches your new project. 34 | 5. 🚀 35 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Example", 3 | "projectDirectory": "{{cookiecutter.projectName|lower|replace(' ', '-')}}-ios", 4 | "teamId": "M8F9QH57A6", 5 | "teamName": "aaa - all about apps Gmbh", 6 | "bundleIdentifier": "at.allaboutapps.{{cookiecutter.projectName|lower|replace(' ', '-')}}", 7 | "deploymentTarget": "16.0", 8 | "texterifyProjectId": "NONE", 9 | "texterifyExportConfigurationId": "NONE", 10 | "defaultLanguageCode": "en", 11 | "runXcodeGen": "y", 12 | "_copy_without_render": ["*.stencil"] 13 | } 14 | -------------------------------------------------------------------------------- /hooks/post_gen_project.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | {%- if cookiecutter.texterifyProjectId != 'NONE' %} 4 | ./buildStrings 5 | {%- endif %} 6 | 7 | {%- if cookiecutter.runXcodeGen == 'y' %} 8 | xcodegen 9 | {%- endif %} 10 | 11 | printf 'all done - enjoy \xf0\x9f\x9a\x80\n' 12 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | printf 'hello from all about apps \xf0\x9f\xa7\xa1\n' -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ## Build generated 4 | #build/ 5 | DerivedData 6 | 7 | ## Various settings 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | 18 | ## Other 19 | *.xccheckout 20 | *.moved-aside 21 | *.xcuserstate 22 | *.xcscmblueprint 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | # Carthage/Build -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Find all the available rules by running: 2 | # swiftlint rules 3 | # 4 | 5 | opt_in_rules: 6 | - unhandled_throwing_task 7 | - empty_count 8 | - first_where 9 | - weak_delegate 10 | - contains_over_filter_is_empty 11 | - contains_over_filter_count 12 | 13 | 14 | disabled_rules: 15 | - todo 16 | - trailing_whitespace 17 | - type_name 18 | - superfluous_disable_command 19 | - force_cast 20 | - identifier_name 21 | - trailing_comma 22 | - cyclomatic_complexity 23 | - blanket_disable_command 24 | - for_where 25 | - nesting 26 | - redundant_optional_initialization 27 | - trailing_newline 28 | 29 | # paths to ignore during linting. Takes precedence over `included`. 30 | excluded: 31 | - Modules/Core/Sources/Assets/Colors.swift 32 | - Modules/Core/Sources/Assets/Images.swift 33 | - Modules/Core/Sources/Assets/Strings.swift 34 | 35 | # configurable rules can be customized from this configuration file 36 | line_length: 37 | warning: 260 38 | error: 400 39 | type_body_length: 40 | warning: 1000 41 | error: 1000 42 | function_body_length: 43 | warning: 300 44 | error: 500 45 | file_length: 46 | warning: 1000 47 | error: 1200 48 | 49 | force_try: 50 | severity: warning 51 | large_tuple: 52 | warning: 5 53 | error: 7 54 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Core", 7 | defaultLocalization: "de", 8 | platforms: [.iOS(.v16)], 9 | products: [ 10 | .library(name: "Assets", targets: ["Assets"]), 11 | .library(name: "CommonUI", targets: ["CommonUI"]), 12 | .library(name: "Models", targets: ["Models"]), 13 | .library(name: "Networking", targets: ["Networking"]), 14 | .library(name: "Utilities", targets: ["Utilities"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/allaboutapps/Fetch.git", from: "3.0.0"), 18 | .package(url: "https://github.com/allaboutapps/Logbook.git", from: "1.1.0"), 19 | .package(url: "https://github.com/allaboutapps/Toolbox.git", from: "5.0.0"), 20 | .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), 21 | .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.2.0"), 22 | ], 23 | targets: [ 24 | .target( 25 | name: "Assets", 26 | dependencies: ["Logbook", "Toolbox", "KeychainAccess"] 27 | ), 28 | .target( 29 | name: "CommonUI", 30 | dependencies: ["Assets", "Models", "Utilities", "Logbook", "Toolbox", "AlamofireImage"] 31 | ), 32 | .target( 33 | name: "Models", 34 | dependencies: ["Logbook", "Toolbox", "KeychainAccess"] 35 | ), 36 | .target( 37 | name: "Networking", 38 | dependencies: ["Models", "Utilities", "Fetch", "Logbook", "Toolbox", "KeychainAccess"], 39 | resources: [ 40 | .process("Stubs"), 41 | ] 42 | ), 43 | .target( 44 | name: "Utilities", 45 | dependencies: ["Fetch", "Logbook", "Toolbox", "KeychainAccess"] 46 | ), 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | TODO -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Colors.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | #if os(macOS) 5 | import AppKit 6 | import SwiftUI 7 | #elseif os(iOS) 8 | import UIKit 9 | import SwiftUI 10 | #elseif os(tvOS) || os(watchOS) 11 | import UIKit 12 | import SwiftUI 13 | #endif 14 | 15 | // Deprecated typealiases 16 | @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") 17 | public typealias AssetColorTypeAlias = ColorAsset.Color 18 | 19 | // swiftlint:disable superfluous_disable_command file_length implicit_return 20 | 21 | // MARK: - Asset Catalogs 22 | 23 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 24 | public enum Colors { 25 | public static let brandPrimary = ColorAsset(name: "brandPrimary") 26 | public static let error = ColorAsset(name: "error") 27 | public static let success = ColorAsset(name: "success") 28 | public static let warning = ColorAsset(name: "warning") 29 | } 30 | 31 | public extension UIColor { 32 | static let brandPrimary = UIColor(named: "brandPrimary", in: BundleToken.bundle, compatibleWith: nil) 33 | static let error = UIColor(named: "error", in: BundleToken.bundle, compatibleWith: nil) 34 | static let success = UIColor(named: "success", in: BundleToken.bundle, compatibleWith: nil) 35 | static let warning = UIColor(named: "warning", in: BundleToken.bundle, compatibleWith: nil) 36 | } 37 | 38 | public extension SwiftUI.Color { 39 | static let brandPrimary = SwiftUI.Color("brandPrimary", bundle: BundleToken.bundle) 40 | static let error = SwiftUI.Color("error", bundle: BundleToken.bundle) 41 | static let success = SwiftUI.Color("success", bundle: BundleToken.bundle) 42 | static let warning = SwiftUI.Color("warning", bundle: BundleToken.bundle) 43 | } 44 | 45 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 46 | 47 | // MARK: - Implementation Details 48 | 49 | public final class ColorAsset { 50 | public fileprivate(set) var name: String 51 | 52 | #if os(macOS) 53 | public typealias Color = NSColor 54 | #elseif os(iOS) || os(tvOS) || os(watchOS) 55 | public typealias Color = UIColor 56 | #endif 57 | 58 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 59 | public private(set) lazy var color: Color = { 60 | guard let color = Color(asset: self) else { 61 | fatalError("Unable to load color asset named \(name).") 62 | } 63 | return color 64 | }() 65 | 66 | #if os(iOS) || os(tvOS) 67 | @available(iOS 11.0, tvOS 11.0, *) 68 | public func color(compatibleWith traitCollection: UITraitCollection) -> Color { 69 | let bundle = BundleToken.bundle 70 | guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { 71 | fatalError("Unable to load color asset named \(name).") 72 | } 73 | return color 74 | } 75 | #endif 76 | 77 | fileprivate init(name: String) { 78 | self.name = name 79 | } 80 | } 81 | 82 | public extension ColorAsset.Color { 83 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 84 | convenience init?(asset: ColorAsset) { 85 | let bundle = BundleToken.bundle 86 | #if os(iOS) || os(tvOS) 87 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 88 | #elseif os(macOS) 89 | self.init(named: NSColor.Name(asset.name), bundle: bundle) 90 | #elseif os(watchOS) 91 | self.init(named: asset.name) 92 | #endif 93 | } 94 | } 95 | 96 | // swiftlint:disable convenience_type 97 | private final class BundleToken { 98 | static let bundle: Bundle = { 99 | #if SWIFT_PACKAGE 100 | return Bundle.module 101 | #else 102 | return Bundle(for: BundleToken.self) 103 | #endif 104 | }() 105 | } 106 | // swiftlint:enable convenience_type 107 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Images.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | #if os(macOS) 5 | import AppKit 6 | #elseif os(iOS) 7 | import UIKit 8 | #elseif os(tvOS) || os(watchOS) 9 | import UIKit 10 | #endif 11 | #if canImport(SwiftUI) 12 | import SwiftUI 13 | #endif 14 | 15 | // Deprecated typealiases 16 | 17 | // swiftlint:disable superfluous_disable_command file_length implicit_return 18 | 19 | // MARK: - Asset Catalogs 20 | 21 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 22 | public enum Images { 23 | } 24 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 25 | 26 | // MARK: - Implementation Details 27 | 28 | // swiftlint:disable convenience_type 29 | private final class BundleToken { 30 | static let bundle: Bundle = { 31 | #if SWIFT_PACKAGE 32 | return Bundle.module 33 | #else 34 | return Bundle(for: BundleToken.self) 35 | #endif 36 | }() 37 | } 38 | // swiftlint:enable convenience_type 39 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Branding/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Branding/brandPrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "osx", 6 | "reference" : "systemGreenColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Feedback/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Feedback/error.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.247", 9 | "green" : "0.200", 10 | "red" : "0.906" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Feedback/success.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.149", 9 | "green" : "0.671", 10 | "red" : "0.333" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Colors.xcassets/Feedback/warning.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.075", 9 | "green" : "0.510", 10 | "red" : "0.941" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Images.xcassets/icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Images.xcassets/illustrations/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/Images.xcassets/logos/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Resources/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "generic_cancel" = "Abbrechen"; 2 | "generic_no" = "Nein"; 3 | "generic_ok" = "Ok"; 4 | "generic_yes" = "Ja"; 5 | "auth_login_button" = "Anmelden"; 6 | "auth_login_title" = "Login"; 7 | "example_title" = "Example"; 8 | "example_text" = "Hello, World!"; 9 | "main_tab_first" = "Tab 1"; 10 | "main_tab_second" = "Tab 2"; 11 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Assets/Strings.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | import Foundation 5 | 6 | // swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references 7 | 8 | // MARK: - Strings 9 | 10 | // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length 11 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces 12 | public enum Strings { 13 | /// Anmelden 14 | public static let authLoginButton = Strings.tr("Localizable", "auth_login_button", fallback: "Anmelden") 15 | /// Login 16 | public static let authLoginTitle = Strings.tr("Localizable", "auth_login_title", fallback: "Login") 17 | /// Hello, World! 18 | public static let exampleText = Strings.tr("Localizable", "example_text", fallback: "Hello, World!") 19 | /// Example 20 | public static let exampleTitle = Strings.tr("Localizable", "example_title", fallback: "Example") 21 | /// Abbrechen 22 | public static let genericCancel = Strings.tr("Localizable", "generic_cancel", fallback: "Abbrechen") 23 | /// Nein 24 | public static let genericNo = Strings.tr("Localizable", "generic_no", fallback: "Nein") 25 | /// Ok 26 | public static let genericOk = Strings.tr("Localizable", "generic_ok", fallback: "Ok") 27 | /// Ja 28 | public static let genericYes = Strings.tr("Localizable", "generic_yes", fallback: "Ja") 29 | /// Tab 1 30 | public static let mainTabFirst = Strings.tr("Localizable", "main_tab_first", fallback: "Tab 1") 31 | /// Tab 2 32 | public static let mainTabSecond = Strings.tr("Localizable", "main_tab_second", fallback: "Tab 2") 33 | } 34 | // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length 35 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces 36 | 37 | // MARK: - Implementation Details 38 | 39 | extension Strings { 40 | private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { 41 | let format = BundleToken.bundle.localizedString(forKey: key, value: value, table: table) 42 | return String(format: format, locale: Locale.current, arguments: args) 43 | } 44 | } 45 | 46 | // swiftlint:disable convenience_type 47 | private final class BundleToken { 48 | static let bundle: Bundle = { 49 | #if SWIFT_PACKAGE 50 | return Bundle.module 51 | #else 52 | return Bundle(for: BundleToken.self) 53 | #endif 54 | }() 55 | } 56 | // swiftlint:enable convenience_type 57 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/CommonUI/Extensions/Storyboard.swift: -------------------------------------------------------------------------------- 1 | // see https://medium.com/swift-programming/uistoryboard-safer-with-enums-protocol-extensions-and-generics-7aad3883b44d 2 | 3 | import UIKit 4 | 5 | // MARK: StoryboardIdentifiable 6 | 7 | public protocol StoryboardIdentifiable { 8 | static var storyboardIdentifier: String { get } 9 | } 10 | 11 | extension UIViewController: StoryboardIdentifiable {} 12 | 13 | public extension StoryboardIdentifiable where Self: UIViewController { 14 | static var storyboardIdentifier: String { 15 | return String(describing: self) 16 | } 17 | } 18 | 19 | // MARK: UIStoryboard 20 | 21 | public extension UIStoryboard { 22 | /// Instantiates a storyboard given its name. 23 | convenience init(_ storyboardName: String, bundle: Bundle? = nil) { 24 | self.init(name: storyboardName, bundle: bundle) 25 | } 26 | 27 | /// Instantiates a typed view controller: 28 | /// ``` 29 | /// let vc: SplashViewController = UIStoryboard(.Misc).instantiateViewController() 30 | /// ``` 31 | func instantiateViewController() -> T { 32 | guard let viewController = self.instantiateViewController(withIdentifier: T.storyboardIdentifier) as? T else { 33 | fatalError("Couldn't instantiate view controller with identifier \(T.storyboardIdentifier) in storyboard \(self)") 34 | } 35 | 36 | return viewController 37 | } 38 | 39 | /// Instantiates a typed view controller: 40 | /// ``` 41 | /// let vc = UIStoryboard(.Misc).instantiateViewController(SplashViewController) 42 | /// ``` 43 | func instantiateViewController(_ type: T.Type) -> T { 44 | guard let viewController = self.instantiateViewController(withIdentifier: type.storyboardIdentifier) as? T else { 45 | fatalError("Couldn't instantiate view controller with identifier \(type.storyboardIdentifier) in storyboard \(self)") 46 | } 47 | 48 | return viewController 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/CommonUI/Style/Appearance.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import Toolbox 3 | import UIKit 4 | 5 | public enum Appearance { 6 | 7 | public static func setup() { 8 | UINavigationBar.appearance().tintColor = .systemOrange 9 | UITabBar.appearance().tintColor = .systemOrange 10 | } 11 | } 12 | 13 | // MARK: - Padding 14 | 15 | public extension Style { 16 | 17 | enum Padding { 18 | /// 4 19 | static let half: CGFloat = 4.0 20 | /// 8 21 | static let single: CGFloat = 8.0 22 | /// 16 23 | static let double: CGFloat = 16.0 24 | /// 24 25 | static let triple: CGFloat = 24.0 26 | } 27 | } 28 | 29 | // MARK: - CornerRadius 30 | 31 | public extension Style { 32 | 33 | enum CornerRadius { 34 | /// 3 35 | static let small: CGFloat = 3.0 36 | /// 5 37 | static let normal: CGFloat = 5.0 38 | /// 10 39 | static let double: CGFloat = 10.0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/CommonUI/ViewState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ViewStateContent {} 4 | 5 | public enum ViewState { 6 | case idle 7 | case loading 8 | case failed(Error) 9 | case empty 10 | case content(Content, Error?) 11 | } 12 | 13 | public extension ViewState { 14 | 15 | var isIdle: Bool { 16 | if case .idle = self { 17 | return true 18 | } else { 19 | return false 20 | } 21 | } 22 | 23 | var contentValue: Content? { 24 | guard case .content(let value, _) = self else { return nil } 25 | return value 26 | } 27 | 28 | mutating func endLoadingWithError(_ error: Error) { 29 | if let value = contentValue { 30 | self = .content(value, error) 31 | } else { 32 | self = .failed(error) 33 | } 34 | } 35 | 36 | mutating func endLoading(_ value: Content?) { 37 | if let value = value { 38 | if let array = value as? [ViewStateContent], array.isEmpty { 39 | self = .empty 40 | } else { 41 | self = .content(value, nil) 42 | } 43 | } else { 44 | self = .empty 45 | } 46 | } 47 | 48 | mutating func startLoading() { 49 | guard contentValue == nil else { return } 50 | self = .loading 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Models/Item.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Item: Codable, Hashable { 4 | public let name: String 5 | 6 | public init(name: String) { 7 | self.name = name 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/API.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Fetch 3 | import Foundation 4 | import Models 5 | import Utilities 6 | 7 | public enum API { 8 | public static func setup() { 9 | APIClient.shared.setup(with: Fetch.Config( 10 | baseURL: Config.API.baseURL, 11 | timeout: Config.API.timeout, 12 | eventMonitors: [APILogger(verbose: Config.API.verboseLogging)], 13 | interceptor: AuthHandler(), 14 | jsonDecoder: Decoders.standardJSON, 15 | cache: MemoryCache(defaultExpiration: .seconds(Config.Cache.defaultExpiration)), 16 | shouldStub: Config.API.stubRequests) 17 | ) 18 | 19 | registerStubs() 20 | } 21 | 22 | static func registerStubs() { 23 | let itemsStubResponse = StubResponse(statusCode: 200, fileName: "items.json", delay: 0.5, bundle: Bundle.module) 24 | APIClient.shared.stubProvider.register(stub: itemsStubResponse, for: API.Example.list()) 25 | } 26 | 27 | public enum Auth { 28 | public static func login(username: String, password: String) -> Resource { 29 | return Resource( 30 | method: .post, 31 | path: "/api/v1/auth/login", 32 | body: .encodable([ 33 | "grantType": "password", 34 | "scope": "user", 35 | "username": username, 36 | "password": password 37 | ]) 38 | ) 39 | } 40 | 41 | public static func tokenRefresh(_ refreshToken: String) -> Resource { 42 | return Resource( 43 | method: .post, 44 | path: "/api/v1/auth/refresh", 45 | body: .encodable([ 46 | "grantType": "refreshToken", 47 | "scope": "user", 48 | "refreshToken": refreshToken 49 | ]) 50 | ) 51 | } 52 | } 53 | 54 | public enum Example { 55 | public static func list() -> Resource<[Item]> { 56 | return Resource( 57 | method: .get, 58 | path: "/example") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/AuthHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Alamofire 3 | import Fetch 4 | import Models 5 | 6 | public class AuthHandler: RequestInterceptor { 7 | 8 | private typealias RefreshCompletion = (_ credentials: Credentials?, _ statusCode: Int?) -> Void 9 | private typealias RequestRetryCompletion = (Alamofire.RetryResult) -> Void 10 | 11 | private static let apiLogger = APILogger(verbose: true) 12 | 13 | private let session: Session = { 14 | let configuration = URLSessionConfiguration.default 15 | return Session(configuration: configuration, eventMonitors: [apiLogger]) 16 | }() 17 | 18 | private let lock = NSLock() 19 | private let queue = DispatchQueue(label: "network.auth.queue") 20 | private var isRefreshing = false 21 | private var requestsToRetry: [RequestRetryCompletion] = [] 22 | 23 | // MARK: - RequestAdapter 24 | 25 | public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { 26 | var urlRequest = urlRequest 27 | if let accessToken = CredentialsController.shared.currentCredentials?.accessToken { 28 | urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") 29 | } 30 | completion(.success(urlRequest)) 31 | } 32 | 33 | // MARK: - RequestRetrier 34 | 35 | public func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: @escaping (Alamofire.RetryResult) -> Void) { 36 | lock.lock() ; defer { lock.unlock() } 37 | 38 | guard let response = request.task?.response as? HTTPURLResponse, 39 | let refreshToken = CredentialsController.shared.currentCredentials?.refreshToken, 40 | response.statusCode == 401 else { 41 | completion(.doNotRetry) 42 | return 43 | } 44 | 45 | requestsToRetry.append(completion) 46 | 47 | if !isRefreshing { 48 | refreshCredentials(refreshToken) { [weak self] (credentials, statusCode) in 49 | guard let self = self else { return } 50 | 51 | self.lock.lock() ; defer { self.lock.unlock() } 52 | 53 | if let credentials = credentials { 54 | CredentialsController.shared.currentCredentials = credentials 55 | self.requestsToRetry.forEach { $0(.retry) } 56 | } else { 57 | if statusCode == 401 { 58 | CredentialsController.shared.currentCredentials = nil 59 | } 60 | self.requestsToRetry.forEach { $0(.doNotRetry) } 61 | } 62 | 63 | self.requestsToRetry.removeAll() 64 | } 65 | } 66 | } 67 | 68 | // MARK: - Private - Refresh Tokens 69 | 70 | private func refreshCredentials(_ refreshToken: String, completion: @escaping RefreshCompletion) { 71 | guard !isRefreshing else { return } 72 | 73 | isRefreshing = true 74 | 75 | guard let urlRequest = try? API.Auth.tokenRefresh(refreshToken).asURLRequest() else { 76 | completion(nil, nil) 77 | return 78 | } 79 | 80 | session 81 | .request(urlRequest) 82 | .validate() 83 | .responseDecodable(queue: queue, completionHandler: { [weak self] (response: DataResponse) in 84 | guard let self = self else { return } 85 | 86 | let statusCode = response.response?.statusCode 87 | 88 | switch response.result { 89 | case .success(let credentials): 90 | completion(credentials, statusCode) 91 | case .failure: 92 | completion(nil, statusCode) 93 | } 94 | self.isRefreshing = false 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/CredentialsController.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import KeychainAccess 4 | import Logbook 5 | import Models 6 | import Utilities 7 | 8 | public struct Credentials: Codable { 9 | public let accessToken: String 10 | public let refreshToken: String 11 | public let expiresIn: TimeInterval 12 | 13 | public init(accessToken: String, refreshToken: String?, expiresIn: TimeInterval?) { 14 | self.accessToken = accessToken 15 | self.refreshToken = refreshToken ?? "" 16 | self.expiresIn = expiresIn ?? NSDate.distantFuture.timeIntervalSinceReferenceDate 17 | } 18 | } 19 | 20 | public class CredentialsController { 21 | private init() {} 22 | 23 | public static let shared = CredentialsController() 24 | 25 | private let keychain = Keychain(service: Config.keyPrefix) 26 | private let credentialStorageKey = Config.Keychain.credentialStorageKey 27 | private var cachedCredentials: Credentials? 28 | 29 | private let jsonDecoder = JSONDecoder() 30 | private let jsonEncoder = JSONEncoder() 31 | 32 | public var currentCredentialsDidChange = PassthroughSubject() 33 | 34 | public var currentCredentials: Credentials? { 35 | get { 36 | if let credentialsData = keychain[data: credentialStorageKey], let credentials = try? jsonDecoder.decode(Credentials.self, from: credentialsData), cachedCredentials == nil { 37 | cachedCredentials = credentials 38 | return credentials 39 | } else { 40 | return cachedCredentials 41 | } 42 | } 43 | set { 44 | if let credentials = newValue { 45 | keychain[data: credentialStorageKey] = try? jsonEncoder.encode(credentials) 46 | cachedCredentials = credentials 47 | } else { 48 | cachedCredentials = nil 49 | _ = try? keychain.remove(credentialStorageKey) 50 | } 51 | currentCredentialsDidChange.send(newValue) 52 | } 53 | } 54 | 55 | public func resetOnNewInstallations() { 56 | if let installationDate = UserDefaults.standard.value(forKey: "installationDate") as? Date { 57 | Logbook.debug("existing installation, app installed: \(installationDate)") 58 | } else { 59 | Logbook.debug("new installation, resetting credentials in keychain") 60 | 61 | if currentCredentials != nil { 62 | currentCredentials = nil 63 | } 64 | 65 | UserDefaults.standard.set(Date(), forKey: "installationDate") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Networking/Stubs/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "all" 4 | }, 5 | { 6 | "name": "about" 7 | }, 8 | { 9 | "name": "apps" 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Utilities/Config.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Global set of configuration values for this application. 4 | public enum Config { 5 | public static let keyPrefix = "at.allaboutapps" 6 | 7 | // MARK: API 8 | 9 | public enum API { 10 | public static var baseURL: URL { 11 | switch AppEnvironment.current.serverEnvironment { 12 | case .dev: 13 | return URL(string: "https://example-dev.allaboutapps.at/api/v1")! 14 | case .staging: 15 | return URL(string: "https://example-staging.allaboutapps.at/api/v1")! 16 | case .live: 17 | return URL(string: "https://example.allaboutapps.at/api/v1")! 18 | } 19 | } 20 | 21 | public static let stubRequests = true 22 | public static var timeout: TimeInterval = 120.0 23 | 24 | public static var verboseLogging: Bool { 25 | switch AppEnvironment.current.buildConfig { 26 | case .debug: 27 | return true 28 | case .release: 29 | return false 30 | } 31 | } 32 | } 33 | 34 | // MARK: Cache 35 | 36 | public enum Cache { 37 | public static let defaultExpiration: TimeInterval = 5 * 60.0 38 | } 39 | 40 | // MARK: User Defaults 41 | 42 | public enum UserDefaultsKey { 43 | public static let lastUpdate = Config.keyPrefix + ".lastUpdate" 44 | } 45 | 46 | // MARK: Keychain 47 | 48 | public enum Keychain { 49 | public static let credentialStorageKey = "CredentialsStorage" 50 | public static let credentialsKey = "credentials" 51 | } 52 | 53 | // MARK: Force Update 54 | 55 | public enum ForceUpdate { 56 | /// URL of the statically hosted version file, used by ForceUpdate package. 57 | public static let publicVersionURL = URL(string: "https://public.allaboutapps.at/config/{{cookiecutter.projectName|lower|replace(' ', '-')}}/version.json")! 58 | } 59 | 60 | // MARK: Debug 61 | 62 | public enum Debug { 63 | 64 | /// Feature flag that determines if the debug feature is enabled. 65 | public static let enabled: Bool = true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Utilities/Decoders.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Decoders { 4 | public static let standardJSON: JSONDecoder = { 5 | let decoder = JSONDecoder() 6 | decoder.dateDecodingStrategy = .custom(Decoders.decodeDate) 7 | return decoder 8 | }() 9 | 10 | public static func decodeDate(decoder: Decoder) throws -> Date { 11 | let container = try decoder.singleValueContainer() 12 | let raw = try container.decode(String.self) 13 | 14 | if let value = try? Date(raw, strategy: .isoDate) { 15 | return value 16 | } 17 | 18 | if let value = try? Date(raw, strategy: .apiDate) { 19 | return value 20 | } else { 21 | throw DecodingError.dataCorruptedError( 22 | in: container, 23 | debugDescription: "Couldn't decode Date from \(raw)." 24 | ) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Utilities/Environment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Defines the environment the app is currently running. 4 | /// The environment is determined by the target's Configuration name 5 | public struct AppEnvironment: Equatable { 6 | 7 | /// Represents base build configurations 8 | public enum BuildConfig: String { 9 | case debug, release 10 | } 11 | 12 | /// The build config of the Environment 13 | public let buildConfig: BuildConfig 14 | 15 | /// Represents environment referring to the used backend 16 | public enum ServerEnvironment: String { 17 | case live, dev, staging 18 | } 19 | 20 | /// The sever environment 21 | public let serverEnvironment: ServerEnvironment 22 | 23 | /// Returns the current environment the app is currently running. 24 | public static var current: AppEnvironment = { 25 | guard let configurationString = Bundle.main.infoDictionary!["_Configuration"] as? String else { 26 | fatalError("Info.plist does not contain the key _Configuration. Add this key with value $(CONFIGURATION)") 27 | } 28 | 29 | guard let serverEnvironmentString = Bundle.main.infoDictionary!["_ServerEnvironment"] as? String else { 30 | fatalError("Info.plist does not contain the key _ServerEnvironment. Add this key with value $(SERVER_ENVIRONMENT)") 31 | } 32 | 33 | let split = configurationString.components(separatedBy: "-") 34 | 35 | guard 36 | split.count == 2, 37 | let buildConfig = BuildConfig(rawValue: split[0].lowercased()), 38 | let serverEnvironment = ServerEnvironment(rawValue: serverEnvironmentString.lowercased()) else { 39 | fatalError("Invalid build configuration") 40 | } 41 | 42 | return AppEnvironment(buildConfig: buildConfig, serverEnvironment: serverEnvironment) 43 | }() 44 | 45 | /// Returns the current App version, build number and environment 46 | /// e.g. `1.0 (3) release-dev` 47 | public var appInfo: String { 48 | guard let infoDict = Bundle.main.infoDictionary, 49 | let versionNumber = infoDict["CFBundleShortVersionString"], 50 | let buildNumber = infoDict["CFBundleVersion"] else { 51 | return "" 52 | } 53 | 54 | return "\(versionNumber) (\(buildNumber)) \(description)" 55 | } 56 | 57 | public static func envVar(named name: String) -> String? { 58 | return ProcessInfo.processInfo.environment[name] 59 | } 60 | } 61 | 62 | extension AppEnvironment: CustomStringConvertible { 63 | public var description: String { 64 | return "\(serverEnvironment.rawValue)-\(buildConfig.rawValue)" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Utilities/FormatStyles.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - ISODateFormatStyle 4 | 5 | /// Parsing input: any ISO Date (e.g. 2024-04-03T09:57:12.898Z). 6 | /// Formatting output: 2024-04-03T09:57:12.898Z 7 | public struct ISODateFormatStyle: ParseableFormatStyle, ParseStrategy { 8 | public var parseStrategy = Date.ISO8601FormatStyle() 9 | .year() 10 | .month() 11 | .day() 12 | .timeZone(separator: .omitted) 13 | .time(includingFractionalSeconds: true) 14 | .timeSeparator(.colon) 15 | 16 | public func format(_ value: Date) -> String { 17 | value.formatted(parseStrategy) 18 | } 19 | 20 | public func parse(_ value: String) throws -> Date { 21 | try Date(value, strategy: parseStrategy) 22 | } 23 | } 24 | 25 | public extension FormatStyle where Self == ISODateFormatStyle { 26 | static var isoDate: ISODateFormatStyle { .init() } 27 | } 28 | 29 | public extension ParseStrategy where Self == ISODateFormatStyle { 30 | static var isoDate: ISODateFormatStyle { .init() } 31 | } 32 | 33 | // MARK: - APIDateFormatStyle 34 | 35 | /// Parsing input: any API Date (e.g. 03.04.2024 12:04). 36 | /// Formatting output: 03.04.2024, 12:04 37 | public struct APIDateFormatStyle: ParseableFormatStyle, ParseStrategy { 38 | public var parseStrategy = Date.FormatStyle() 39 | .day(.twoDigits) 40 | .month(.twoDigits) 41 | .year(.extended(minimumLength: 4)) 42 | .hour(.twoDigits(amPM: .omitted)) 43 | .minute(.twoDigits) 44 | 45 | public func format(_ value: Date) -> String { 46 | value.formatted(parseStrategy) 47 | } 48 | 49 | public func parse(_ value: String) throws -> Date { 50 | try Date(value, strategy: parseStrategy) 51 | } 52 | } 53 | 54 | public extension FormatStyle where Self == APIDateFormatStyle { 55 | static var apiDate: APIDateFormatStyle { .init() } 56 | } 57 | 58 | public extension ParseStrategy where Self == APIDateFormatStyle { 59 | static var apiDate: APIDateFormatStyle { .init() } 60 | } 61 | 62 | // MARK: - LocalizedWeekDayFormatStyle 63 | 64 | /// Input: any Date (e.g. 3 Apr 2024 at 12:16 PM). 65 | /// Formatting output: Wed, 3 Apr 66 | public struct LocalizedWeekDayFormatStyle: FormatStyle { 67 | public func format(_ value: Date) -> String { 68 | value.formatted( 69 | Date.FormatStyle() 70 | .weekday(.abbreviated) 71 | .day(.defaultDigits) 72 | .month(.abbreviated) 73 | .locale(.autoupdatingCurrent) 74 | ) 75 | } 76 | } 77 | 78 | public extension FormatStyle where Self == LocalizedWeekDayFormatStyle { 79 | static var localizedWeekDay: LocalizedWeekDayFormatStyle { .init() } 80 | } 81 | 82 | // MARK: - LocalizedWeekdayWithYearFormatStyle 83 | 84 | /// Input: any Date (e.g. 3 Apr 2024 at 12:16 PM). 85 | /// Formatting output: Wed, 3 Apr 2024 86 | public struct LocalizedWeekdayWithYearFormatStyle: FormatStyle { 87 | public func format(_ value: Date) -> String { 88 | value.formatted( 89 | Date.FormatStyle() 90 | .weekday(.abbreviated) 91 | .day(.defaultDigits) 92 | .month(.abbreviated) 93 | .year() 94 | .locale(.autoupdatingCurrent) 95 | ) 96 | } 97 | } 98 | 99 | public extension FormatStyle where Self == LocalizedWeekdayWithYearFormatStyle { 100 | static var localizedWeekdayWithYear: LocalizedWeekdayWithYearFormatStyle { .init() } 101 | } 102 | 103 | // MARK: - MediumDateFormatStyle 104 | 105 | /// Input: any Date (e.g. 3 Apr 2024 at 12:16 PM). 106 | /// Formatting output: 3 Apr 2024 107 | public struct MediumDateFormatStyle: FormatStyle { 108 | public func format(_ value: Date) -> String { 109 | value.formatted(date: .abbreviated, time: .omitted) 110 | } 111 | } 112 | 113 | public extension FormatStyle where Self == MediumDateFormatStyle { 114 | static var medium: MediumDateFormatStyle { .init() } 115 | } 116 | 117 | // MARK: - HourMinuteShortFormatStyle 118 | 119 | /// Input: any Date Range (e.g. 2 Apr 2024 at 12:21 PM - 3 Apr 2024 at 12:15 PM). 120 | /// Formatting output: 23 hrs, 53 min 121 | public struct HourMinuteShortFormatStyle: FormatStyle { 122 | public func format(_ value: Range) -> String { 123 | let style = Date.ComponentsFormatStyle( 124 | style: .abbreviated, 125 | locale: .autoupdatingCurrent, 126 | calendar: .init(identifier: .gregorian), 127 | fields: [.hour, .minute] 128 | ) 129 | 130 | return value.formatted(style) 131 | } 132 | } 133 | 134 | public extension FormatStyle where Self == HourMinuteShortFormatStyle { 135 | static var hourMinuteShort: HourMinuteShortFormatStyle { .init() } 136 | } 137 | 138 | // MARK: - CurrencyEuroFormatStyle 139 | 140 | /// Input: any Double (e.g. 151.498). 141 | /// Formatting output: € 151,50 142 | public struct CurrencyEuroFormatStyle: FormatStyle { 143 | public func format(_ value: Double) -> String { 144 | value.formatted( 145 | .currency(code: "EUR") 146 | .precision(.fractionLength(2)) 147 | .locale(.init(identifier: "de-AT")) 148 | .rounded() 149 | ) 150 | } 151 | } 152 | 153 | public extension FormatStyle where Self == CurrencyEuroFormatStyle { 154 | static var currencyEuro: CurrencyEuroFormatStyle { .init() } 155 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Sources/Utilities/Logging.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Logbook 3 | 4 | // MARK: - Global 5 | 6 | public let log = Logbook.self 7 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Core/Tests/CoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Kit 3 | 4 | final class CoreTests: XCTestCase { 5 | func testExample() throws { 6 | XCTAssertEqual(2, 1+1) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Features", 7 | defaultLocalization: "de", 8 | platforms: [.iOS(.v16)], 9 | products: [ 10 | .library(name: "MainFeature", targets: ["MainFeature"]), 11 | .library(name: "AuthFeature", targets: ["AuthFeature"]), 12 | .library(name: "ExampleFeature", targets: ["ExampleFeature"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/allaboutapps/StatefulViewController.git", from: "5.2.0"), 16 | .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.2.0"), 17 | .package(url: "https://github.com/allaboutapps/DataSource.git", from: "8.1.3"), 18 | .package(url: "https://github.com/allaboutapps/debugview-ios", from: "1.0.0"), 19 | .package(path: "../Core"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "MainFeature", 24 | dependencies: [ 25 | "ExampleFeature", 26 | .product(name: "Assets", package: "Core"), 27 | .product(name: "CommonUI", package: "Core"), 28 | .product(name: "Models", package: "Core"), 29 | .product(name: "Networking", package: "Core"), 30 | .product(name: "Utilities", package: "Core"), 31 | ] 32 | ), 33 | .target( 34 | name: "AuthFeature", 35 | dependencies: [ 36 | "StatefulViewController", 37 | "AlamofireImage", 38 | "DataSource", 39 | .product(name: "Assets", package: "Core"), 40 | .product(name: "CommonUI", package: "Core"), 41 | .product(name: "Models", package: "Core"), 42 | .product(name: "Networking", package: "Core"), 43 | .product(name: "Utilities", package: "Core"), 44 | ] 45 | ), 46 | .target( 47 | name: "ExampleFeature", 48 | dependencies: [ 49 | "StatefulViewController", 50 | "AlamofireImage", 51 | "DataSource", 52 | .product(name: "Assets", package: "Core"), 53 | .product(name: "CommonUI", package: "Core"), 54 | .product(name: "Models", package: "Core"), 55 | .product(name: "Networking", package: "Core"), 56 | .product(name: "Utilities", package: "Core"), 57 | .product(name: "DebugView", package: "debugview-ios"), 58 | ] 59 | ), 60 | ] 61 | ) 62 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/AuthFeature/AuthCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Toolbox 2 | import UIKit 3 | 4 | public class AuthCoordinator: NavigationCoordinator { 5 | 6 | // MARK: Interface 7 | 8 | public var onLogin: (() -> Void)? 9 | 10 | // MARK: - Init 11 | 12 | override public init(navigationController: UINavigationController) { 13 | super.init(navigationController: navigationController) 14 | navigationController.isModalInPresentation = true 15 | } 16 | 17 | // MARK: Start 18 | 19 | override public func start() { 20 | let viewController = LoginViewController.create() 21 | viewController.onLogin = onLogin 22 | 23 | push(viewController, animated: false) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/AuthFeature/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import Networking 3 | import Toolbox 4 | import UIKit 5 | 6 | class LoginViewController: UIViewController { 7 | 8 | // MARK: Interface 9 | 10 | var onLogin: (() -> Void)! 11 | 12 | static func create() -> LoginViewController { 13 | let viewController = LoginViewController() 14 | return viewController 15 | } 16 | 17 | // MARK: Views 18 | 19 | private lazy var loginButton = UIButton().with { 20 | $0.translatesAutoresizingMaskIntoConstraints = false 21 | $0.setTitle(Strings.authLoginButton, for: .normal) 22 | $0.addTarget(self, action: #selector(handleLoginButtonTapped), for: .touchUpInside) 23 | $0.setTitleColor(.brandPrimary, for: .normal) 24 | } 25 | 26 | // MARK: Private 27 | 28 | // viewModel, computed properties etc. 29 | 30 | // MARK: Lifecycle 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | setupViews() 36 | setupConstraints() 37 | } 38 | 39 | // MARK: Setup 40 | 41 | private func setupViews() { 42 | view.backgroundColor = .white 43 | title = Strings.authLoginTitle 44 | 45 | view.addSubview(loginButton) 46 | } 47 | 48 | private func setupConstraints() { 49 | loginButton.withConstraints { 50 | $0.alignCenter() 51 | } 52 | } 53 | 54 | // MARK: Actions 55 | 56 | @objc private func handleLoginButtonTapped() { 57 | CredentialsController.shared.currentCredentials = Credentials(accessToken: "testToken", refreshToken: nil, expiresIn: nil) 58 | onLogin() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/ExampleFeature/ExampleCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import DebugView 3 | import Networking 4 | import SwiftUI 5 | import Toolbox 6 | import UIKit 7 | 8 | public class ExampleCoordinator: NavigationCoordinator { 9 | // MARK: Init 10 | 11 | public init(title: String, navigationController: UINavigationController = UINavigationController()) { 12 | super.init(navigationController: navigationController) 13 | 14 | navigationController.tabBarItem.title = title 15 | navigationController.tabBarItem.image = UIImage(systemName: "star") 16 | navigationController.tabBarItem.selectedImage = UIImage(systemName: "star.fill") 17 | navigationController.navigationBar.prefersLargeTitles = true 18 | navigationController.navigationBar.tintColor = UIColor.brandPrimary 19 | } 20 | 21 | // MARK: Start 22 | 23 | override public func start() { 24 | let viewController = UIHostingController( 25 | rootView: ExampleScreen( 26 | outAction: { [weak self] action in 27 | switch action { 28 | case .debug: 29 | self?.presentDebug() 30 | } 31 | } 32 | ) 33 | ) 34 | 35 | viewController.navigationItem.title = Strings.exampleTitle 36 | viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "rectangle.portrait.and.arrow.right"), style: .plain, target: self, action: #selector(logout)) 37 | viewController.navigationItem.largeTitleDisplayMode = .always 38 | 39 | push(viewController, animated: false) 40 | } 41 | 42 | // MARK: Present 43 | 44 | private func presentDebug() { 45 | let viewController = UIHostingController( 46 | rootView: DebugScreen( 47 | controller: Services.shared[DebugController.self], 48 | appearance: .init(tintColor: .brandPrimary) 49 | ) 50 | ) 51 | 52 | viewController.navigationItem.leftBarButtonItem = .init( 53 | systemItem: .close, 54 | primaryAction: .init(handler: { [weak viewController] _ in 55 | viewController?.dismiss(animated: true) 56 | }) 57 | ) 58 | 59 | viewController.title = "Debug" 60 | 61 | let navController = UINavigationController(rootViewController: viewController) 62 | navController.navigationBar.prefersLargeTitles = true 63 | 64 | present(navController, animated: true) 65 | } 66 | 67 | // MARK: Helpers 68 | 69 | @objc private func logout() { 70 | CredentialsController.shared.currentCredentials = nil 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/ExampleFeature/ExampleScreen.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import Models 3 | import Networking 4 | import SwiftUI 5 | 6 | struct ExampleScreen: View { 7 | 8 | // MARK: Init 9 | 10 | enum OutAction { 11 | case debug 12 | } 13 | 14 | private let sendOutAction: (OutAction) -> Void 15 | 16 | init(outAction: @escaping (OutAction) -> Void) { 17 | sendOutAction = outAction 18 | _viewModel = StateObject(wrappedValue: ExampleViewModel()) 19 | } 20 | 21 | // MARK: Properties 22 | 23 | @StateObject private var viewModel: ExampleViewModel 24 | 25 | // MARK: Body 26 | 27 | var body: some View { 28 | Group { 29 | switch viewModel.viewState { 30 | case .content(let items, _): 31 | content(items: items) 32 | case .empty: 33 | Text("No data") 34 | case .failed: 35 | Text("Error") 36 | case .loading: 37 | ProgressView() 38 | case .idle: 39 | EmptyView() 40 | } 41 | } 42 | // on iOS 15 we should use the task modifier 43 | .onAppear { 44 | guard viewModel.viewState.isIdle else { return } 45 | 46 | Task { 47 | await viewModel.fetch() 48 | } 49 | } 50 | } 51 | 52 | // MARK: Helpers 53 | 54 | private func content(items: [Item]) -> some View { 55 | List { 56 | Section { 57 | ForEach(items, id: \.self) { item in 58 | Text(item.name) 59 | } 60 | } 61 | Section { 62 | Button( 63 | action: { 64 | sendOutAction(.debug) 65 | }, 66 | label: { 67 | Text("Debug") 68 | } 69 | ) 70 | } 71 | } 72 | .foregroundColor(.brandPrimary) 73 | } 74 | } 75 | 76 | // MARK: - Previews 77 | 78 | struct ExampleScreen_Previews: PreviewProvider { 79 | 80 | static var previews: some View { 81 | ExampleScreen(outAction: { _ in }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/ExampleFeature/ExampleViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CommonUI 3 | import Foundation 4 | import Models 5 | import Networking 6 | import Utilities 7 | 8 | @MainActor 9 | class ExampleViewModel: ObservableObject { 10 | 11 | @Published var viewState: ViewState<[Item]> = .idle 12 | 13 | var cancellable: AnyCancellable? 14 | 15 | func fetch() async { 16 | viewState.startLoading() 17 | 18 | do { 19 | let items = try await API.Example.list().requestAsync().model 20 | viewState.endLoading(items) 21 | } catch { 22 | viewState.endLoadingWithError(error) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Sources/MainFeature/MainCoordinator.swift: -------------------------------------------------------------------------------- 1 | import Assets 2 | import ExampleFeature 3 | import Toolbox 4 | import UIKit 5 | 6 | public class MainCoordinator: TabBarCoordinator { 7 | 8 | // MARK: - Properties 9 | 10 | private lazy var firstCoordinator = ExampleCoordinator(title: Strings.mainTabFirst) 11 | private lazy var secondCoordinator = ExampleCoordinator(title: Strings.mainTabSecond) 12 | 13 | // MARK: - Tabs 14 | 15 | private enum Tab: Int, CaseIterable { 16 | case first = 0 17 | case second 18 | } 19 | 20 | private func setupTabs() { 21 | addChild(firstCoordinator) 22 | addChild(secondCoordinator) 23 | 24 | firstCoordinator.start() 25 | secondCoordinator.start() 26 | 27 | tabBarController.viewControllers = [ 28 | firstCoordinator.rootViewController, 29 | secondCoordinator.rootViewController, 30 | ] 31 | 32 | tabBarController.selectedIndex = 0 33 | tabBarController.tabBar.tintColor = UIColor.brandPrimary 34 | } 35 | 36 | // MARK: - Coordinator Start 37 | 38 | override public func start() { 39 | setupTabs() 40 | } 41 | 42 | public func reset(animated: Bool) { 43 | childCoordinators.forEach { 44 | ($0 as? NavigationCoordinator)?.popToRoot(animated: animated) 45 | } 46 | tabBarController.selectedIndex = 0 47 | } 48 | 49 | // MARK: - Helpers 50 | 51 | private func setTab(_ tab: Tab) { 52 | tabBarController.selectedIndex = tab.rawValue 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Modules/Features/Tests/FeaturesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Kit 3 | 4 | final class FeaturesTests: XCTestCase { 5 | func testExample() throws { 6 | XCTAssertEqual(2, 1+1) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/README.md: -------------------------------------------------------------------------------- 1 | ### ForceUpdate 2 | 3 | The project comes with the ability to show a blocker screen, if needed. This blocker screen tells the user to update their app. 4 | 5 | Change the path of the statically hosted version file at `Core.Utilities.Config.ForceUpdate.publicVersionURL`: 6 | 7 | ```swift 8 | public enum ForceUpdate { 9 | 10 | /// url of the statically hosted version file, used by force update feature 11 | public static let publicVersionURL = URL(string: "https://public.allaboutapps.at/config/test/version.json")! 12 | } 13 | ``` 14 | 15 | [Force Update GitHub](https://github.com/allaboutapps/force-update-ios) 16 | 17 | ### Debug 18 | 19 | The project comes with a pre-configured debug screen that can easily be extended. 20 | 21 | #### Disable feature 22 | 23 | By default, it is enabled. However, it can be disabled in `Core.Utilities.Config.Debug.enabled`: 24 | 25 | [DebugView GitHub](https://github.com/allaboutapps/force-update-ios) -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '4' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: ios 5 | trigger_map: 6 | - push_branch: development 7 | workflow: dev 8 | - push_branch: main 9 | workflow: main 10 | - push_branch: testflight 11 | workflow: testflight 12 | - pull_request_source_branch: "*" 13 | workflow: main 14 | pull_request_target_branch: main 15 | workflows: 16 | _kickstart: 17 | steps: 18 | - activate-ssh-key: 19 | {% raw %}run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'{% endraw %} 20 | - git-clone: 21 | inputs: 22 | - clone_depth: '1' 23 | - cache-pull: {} 24 | - brew-install: 25 | inputs: 26 | - upgrade: 'no' 27 | - cache_enabled: 'yes' 28 | - packages: licenseplist 29 | title: 'Brew install: LicensePlist' 30 | - brew-install: 31 | inputs: 32 | - upgrade: 'no' 33 | - cache_enabled: 'yes' 34 | - packages: swiftgen 35 | title: 'Brew install: SwiftGen' 36 | - brew-install: 37 | title: 'Brew install: XcodeGen' 38 | inputs: 39 | - upgrade: 'no' 40 | - cache_enabled: 'yes' 41 | - packages: xcodegen 42 | - yarn: 43 | inputs: 44 | - command: global add texterify 45 | title: Install Texterify CLI 46 | - script: 47 | title: Update Texterify Strings 48 | inputs: 49 | - content: |- 50 | #!/usr/bin/env bash 51 | # fail if any commands fails 52 | set -e 53 | # make pipelines' return status equal the last command to exit with a non-zero status, or zero if all commands exit successfully 54 | set -o pipefail 55 | # debug log 56 | set -x 57 | 58 | # write your script here 59 | # download texterify strings 60 | if texterify download --auth-email="$AAA_TXTY_AUTH_EMAIL" --auth-secret="$AAA_TXTY_AUTH_SECRET" 61 | then 62 | # generate strings 63 | swiftgen 64 | else 65 | BRed='\033[1;31m' # Red Bold 66 | NC='\033[0m' # No Color 67 | echo -e "${BRed} Texterify Download failed! Check texterify.json if the project_id is correctly set up. ${NC}" 68 | fi 69 | 70 | exit 0 71 | - cache-push: {} 72 | - remote-script-runner: 73 | inputs: 74 | - script_url: https://public.allaboutapps.at/bitrise/ios-last-commit-date.sh 75 | - set-xcode-build-number: 76 | inputs: 77 | - build_version_offset: '' 78 | - build_version: "$AAA_LAST_COMMIT_DATE" 79 | - plist_path: "$AAA_INFO_PLIST_PATH" 80 | dev: 81 | steps: 82 | - xcode-archive: 83 | inputs: 84 | - project_path: "$BITRISE_PROJECT_PATH" 85 | - export_method: development 86 | - is_clean_build: 'yes' 87 | - automatic_code_signing: api-key 88 | - register_test_devices: 'yes' 89 | - artifact_name: {{cookiecutter.projectName}}-Dev 90 | - scheme: "$BITRISE_SCHEME_DEV" 91 | title: Dev - Xcode Archive & Export 92 | - deploy-to-bitrise-io: 93 | title: Deploy to Bitrise.io 94 | - remote-script-runner: 95 | inputs: 96 | - script_url: https://public.allaboutapps.at/bitrise/ios-push-tag.sh 97 | title: Push Version Tag 98 | before_run: 99 | - _kickstart 100 | main: 101 | steps: 102 | - xcode-archive: 103 | inputs: 104 | - project_path: "$BITRISE_PROJECT_PATH" 105 | - export_method: development 106 | - is_clean_build: 'yes' 107 | - automatic_code_signing: api-key 108 | - register_test_devices: 'yes' 109 | - artifact_name: {{cookiecutter.projectName}}-Staging 110 | - scheme: "$BITRISE_SCHEME_STAGING" 111 | title: Staging - Xcode Archive & Export 112 | - xcode-archive: 113 | inputs: 114 | - project_path: "$BITRISE_PROJECT_PATH" 115 | - export_method: development 116 | - is_clean_build: 'yes' 117 | - automatic_code_signing: api-key 118 | - register_test_devices: 'yes' 119 | - artifact_name: {{cookiecutter.projectName}}-Live 120 | - scheme: "$BITRISE_SCHEME_LIVE" 121 | title: Live - Xcode Archive & Export 122 | - deploy-to-bitrise-io: 123 | title: Deploy to Bitrise.io 124 | - remote-script-runner: 125 | inputs: 126 | - script_url: https://public.allaboutapps.at/bitrise/ios-push-tag.sh 127 | title: Push Version Tag 128 | before_run: 129 | - _kickstart 130 | testflight: 131 | steps: 132 | - xcode-archive: 133 | inputs: 134 | - project_path: "$BITRISE_PROJECT_PATH" 135 | - export_method: app-store 136 | - is_clean_build: 'yes' 137 | - automatic_code_signing: api-key 138 | - register_test_devices: 'yes' 139 | - distribution_method: app-store 140 | - scheme: "$BITRISE_SCHEME_LIVE" 141 | - deploy-to-itunesconnect-application-loader: 142 | inputs: 143 | - connection: api_key 144 | - app_password: "$AAA_APP_SPECIFIC_PW" 145 | - remote-script-runner: 146 | inputs: 147 | - script_url: https://public.allaboutapps.at/bitrise/ios-push-tag.sh 148 | title: Push Version Tag 149 | before_run: 150 | - _kickstart 151 | app: 152 | envs: 153 | - BITRISE_PROJECT_PATH: {{cookiecutter.projectName}}.xcodeproj 154 | - BITRISE_SCHEME_LIVE: {{cookiecutter.projectName}} Live 155 | - BITRISE_SCHEME_DEV: {{cookiecutter.projectName}} Dev 156 | - BITRISE_SCHEME_STAGING: {{cookiecutter.projectName}} Staging 157 | - AAA_INFO_PLIST_PATH: "$BITRISE_SOURCE_DIR/{{cookiecutter.projectName}}/SupportingFiles/Info.plist" 158 | - AAA_APP_BUNDLE_ID_LIVE: {{cookiecutter.bundleIdentifier}} 159 | - AAA_APP_BUNDLE_ID_DEV: {{cookiecutter.bundleIdentifier}}-dev 160 | - AAA_APP_BUNDLE_ID_STAGING: {{cookiecutter.bundleIdentifier}}-staging 161 | - AAA_ITUNES_TEAM: {{cookiecutter.teamName}} 162 | - AAA_DEV_TEAM_ID: {{cookiecutter.teamId}} 163 | - GIT_SSH_COMMAND: ssh -o PubkeyAcceptedAlgorithms=+ssh-rsa 164 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/buildStrings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd $(dirname $0) 3 | 4 | texterify download 5 | swiftgen 6 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/default.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --beforemarks 5 | --binarygrouping 4,8 6 | --categorymark "MARK: %c" 7 | --classthreshold 0 8 | --closingparen balanced 9 | --closurevoid remove 10 | --commas always 11 | --conflictmarkers reject 12 | --decimalgrouping 3,6 13 | --elseposition same-line 14 | --emptybraces no-space 15 | --enumthreshold 0 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --extensionacl on-extension 19 | --extensionlength 0 20 | --extensionmark "MARK: - %t + %c" 21 | --fractiongrouping disabled 22 | --fragment false 23 | --funcattributes preserve 24 | --groupedextension "MARK: %c" 25 | --guardelse auto 26 | --header strip 27 | --hexgrouping 4,8 28 | --hexliteralcase uppercase 29 | --ifdef indent 30 | --importgrouping alpha 31 | --indent 4 32 | --indentcase false 33 | --indentstrings false 34 | --lifecycle 35 | --lineaftermarks true 36 | --linebreaks lf 37 | --markcategories true 38 | --markextensions always 39 | --marktypes always 40 | --maxwidth none 41 | --modifierorder 42 | --nevertrailing 43 | --nospaceoperators 44 | --nowrapoperators 45 | --octalgrouping 4,8 46 | --operatorfunc spaced 47 | --organizetypes actor,class,enum,struct 48 | --patternlet inline 49 | --ranges spaced 50 | --redundanttype infer-locals-only 51 | --self remove 52 | --selfrequired 53 | --semicolons inline 54 | --shortoptionals always 55 | --smarttabs enabled 56 | --stripunusedargs always 57 | --structthreshold 0 58 | --tabwidth unspecified 59 | --trailingclosures 60 | --trimwhitespace always 61 | --typeattributes preserve 62 | --typemark "MARK: - %t" 63 | --varattributes preserve 64 | --voidtype void 65 | --wraparguments preserve 66 | --wrapcollections preserve 67 | --wrapconditions preserve 68 | --wrapparameters default 69 | --wrapreturntype preserve 70 | --wrapternary default 71 | --wraptypealiases preserve 72 | --xcodeindentation disabled 73 | --yodaswap always 74 | --disable blankLinesAtStartOfScope 75 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/project.yml: -------------------------------------------------------------------------------- 1 | name: {{ cookiecutter.projectName }} 2 | options: 3 | xcodeVersion: 14.0 4 | groupOrdering: 5 | - order: [Modules, {{ cookiecutter.projectName }}] 6 | localPackagesGroup: Modules 7 | fileGroups: [README.md] 8 | configs: 9 | Debug-Dev: debug 10 | Debug-Staging: debug 11 | Debug-Live: debug 12 | Release-Dev: release 13 | Release-Staging: release 14 | Release-Live: release 15 | settings: 16 | base: 17 | DEVELOPMENT_TEAM: {{cookiecutter.teamId}} 18 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES 19 | configs: 20 | Debug-Dev: 21 | SWIFT_COMPILATION_MODE: "incremental" 22 | Debug-Staging: 23 | SWIFT_COMPILATION_MODE: "incremental" 24 | Debug-Live: 25 | SWIFT_COMPILATION_MODE: "incremental" 26 | Release-Dev: 27 | SWIFT_COMPILATION_MODE: "wholemodule" 28 | Release-Staging: 29 | SWIFT_COMPILATION_MODE: "wholemodule" 30 | Release-Live: 31 | SWIFT_COMPILATION_MODE: "wholemodule" 32 | attributes: 33 | ORGANIZATIONNAME: {{cookiecutter.teamName}} 34 | packages: 35 | Features: 36 | path: Modules/Features 37 | Core: 38 | path: Modules/Core 39 | ForceUpdate: 40 | url: "https://github.com/allaboutapps/force-update-ios" 41 | from: 1.0.1 42 | DebugView: 43 | url: "https://github.com/allaboutapps/debugview-ios" 44 | from: 1.0.0 45 | targets: 46 | {{cookiecutter.projectName}}: 47 | type: application 48 | platform: iOS 49 | deploymentTarget: {{cookiecutter.deploymentTarget}} 50 | configFiles: 51 | Debug-Dev: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Dev.xcconfig 52 | Debug-Staging: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Staging.xcconfig 53 | Debug-Live: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Live.xcconfig 54 | Release-Dev: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Dev.xcconfig 55 | Release-Staging: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Staging.xcconfig 56 | Release-Live: {{cookiecutter.projectName}}/SupportingFiles/Configurations/Live.xcconfig 57 | settings: 58 | base: 59 | PRODUCT_NAME: {{cookiecutter.projectName}} 60 | MARKETING_VERSION: 1.0.0 61 | CURRENT_PROJECT_VERSION: 1 62 | TARGETED_DEVICE_FAMILY: "1" 63 | sources: 64 | - {{cookiecutter.projectName}} 65 | dependencies: 66 | - package: Features 67 | product: MainFeature 68 | - package: Features 69 | product: AuthFeature 70 | - package: Features 71 | product: ExampleFeature 72 | - package: Core 73 | product: Assets 74 | - package: Core 75 | product: Models 76 | - package: Core 77 | product: Networking 78 | - package: Core 79 | product: Utilities 80 | - package: Core 81 | product: CommonUI 82 | - package: ForceUpdate 83 | product: ForceUpdate 84 | - package: DebugView 85 | product: DebugView 86 | postCompileScripts: 87 | - script: | 88 | export PATH="$PATH:/opt/homebrew/bin" 89 | 90 | if which swiftlint >/dev/null; then 91 | swiftlint 92 | else 93 | echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" 94 | fi 95 | name: SwiftLint 96 | basedOnDependencyAnalysis: false 97 | - script: | 98 | export PATH="$PATH:/opt/homebrew/bin" 99 | 100 | if which /usr/libexec/PlistBuddy >/dev/null; then 101 | version="$MARKETING_VERSION" 102 | build="$CURRENT_PROJECT_VERSION" 103 | /usr/libexec/PlistBuddy "$SRCROOT/$PRODUCT_NAME/SupportingFiles/Settings.bundle/Root.plist" -c "set PreferenceSpecifiers:2:DefaultValue $version ($build)" 104 | else 105 | echo "warning: PlistBuddy not found" 106 | fi 107 | 108 | if which license-plist >/dev/null; then 109 | license-plist --output-path $PRODUCT_NAME/SupportingFiles/Settings.bundle --config-path $PRODUCT_NAME/SupportingFiles/license_plist.yml --package-path $PROJECT_FILE_PATH/project.xcworkspace/xcshareddata/swiftpm/Package.swift --suppress-opening-directory 110 | else 111 | echo "warning: license-plist not installed, download from https://github.com/mono0926/LicensePlist" 112 | fi 113 | name: Generate Licenses 114 | basedOnDependencyAnalysis: false 115 | - script: | 116 | #case "${SERVER_ENVIRONMENT}" in 117 | # 118 | #"dev" ) 119 | #cp -r "${PROJECT_DIR}/{{cookiecutter.projectName}}/SupportingFiles/GoogleService-Info-Dev.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" ;; 120 | # 121 | #"staging" ) 122 | #cp -r "${PROJECT_DIR}/{{cookiecutter.projectName}}/SupportingFiles/GoogleService-Info-Staging.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" ;; 123 | # 124 | #"live" ) 125 | #cp -r "${PROJECT_DIR}/{{cookiecutter.projectName}}/SupportingFiles/GoogleService-Info-Live.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" ;; 126 | # 127 | #*) 128 | #;; 129 | #esac 130 | name: Copy GoogleService-Info 131 | basedOnDependencyAnalysis: false 132 | schemes: 133 | {{cookiecutter.projectName}} Dev: 134 | build: 135 | targets: 136 | {{cookiecutter.projectName}}: all 137 | run: 138 | config: Debug-Dev 139 | profile: 140 | config: Release-Dev 141 | analyze: 142 | config: Release-Dev 143 | archive: 144 | config: Release-Dev 145 | {{cookiecutter.projectName}} Staging: 146 | build: 147 | targets: 148 | {{cookiecutter.projectName}}: all 149 | run: 150 | config: Debug-Staging 151 | profile: 152 | config: Release-Staging 153 | analyze: 154 | config: Release-Staging 155 | archive: 156 | config: Release-Staging 157 | {{cookiecutter.projectName}} Live: 158 | build: 159 | targets: 160 | {{cookiecutter.projectName}}: all 161 | run: 162 | config: Debug-Live 163 | profile: 164 | config: Release-Live 165 | analyze: 166 | config: Release-Live 167 | archive: 168 | config: Release-Live 169 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/swiftgen.yml: -------------------------------------------------------------------------------- 1 | strings: 2 | inputs: Modules/Core/Sources/Assets/Resources/de.lproj 3 | filter: .+\.strings$|.stringsdict$ 4 | outputs: 5 | - templateName: structured-swift5 6 | output: Modules/Core/Sources/Assets/Strings.swift 7 | params: 8 | enumName: Strings 9 | publicAccess: true 10 | xcassets: 11 | # icons 12 | - inputs: 13 | - Modules/Core/Sources/Assets/Resources/Images.xcassets 14 | outputs: 15 | - templateName: swift5 16 | output: Modules/Core/Sources/Assets/Images.swift 17 | params: 18 | enumName: Images 19 | publicAccess: true 20 | # colors 21 | - inputs: 22 | - Modules/Core/Sources/Assets/Resources/Colors.xcassets 23 | outputs: 24 | #- templateName: swift5 25 | - templatePath: templates/assets.stencil 26 | output: Modules/Core/Sources/Assets/Colors.swift 27 | params: 28 | enumName: Colors 29 | publicAccess: true 30 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/templates/assets.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | {% if catalogs %} 5 | {% set enumName %}{{param.enumName|default:"Asset"}}{% endset %} 6 | {% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %} 7 | {% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %} 8 | {% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %} 9 | {% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %} 10 | {% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %} 11 | {% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %} 12 | {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} 13 | #if os(macOS) 14 | import AppKit 15 | import SwiftUI 16 | #elseif os(iOS) 17 | {% if resourceCount.arresourcegroup > 0 %} 18 | import ARKit 19 | {% endif %} 20 | import UIKit 21 | import SwiftUI 22 | #elseif os(tvOS) || os(watchOS) 23 | import UIKit 24 | import SwiftUI 25 | #endif 26 | 27 | // Deprecated typealiases 28 | {% if resourceCount.color > 0 %} 29 | @available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0") 30 | {{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color 31 | {% endif %} 32 | {% if resourceCount.image > 0 %} 33 | @available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0") 34 | {{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image 35 | {% endif %} 36 | 37 | // swiftlint:disable superfluous_disable_command file_length implicit_return 38 | 39 | // MARK: - Asset Catalogs 40 | 41 | {% macro enumBlock assets %} 42 | {% call casesBlock assets %} 43 | {% if param.allValues %} 44 | 45 | // swiftlint:disable trailing_comma 46 | {% if resourceCount.arresourcegroup > 0 %} 47 | {{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [ 48 | {% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %} 49 | ] 50 | {% endif %} 51 | {% if resourceCount.color > 0 %} 52 | {{accessModifier}} static let allColors: [{{colorType}}] = [ 53 | {% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %} 54 | ] 55 | {% endif %} 56 | {% if resourceCount.data > 0 %} 57 | {{accessModifier}} static let allDataAssets: [{{dataType}}] = [ 58 | {% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %} 59 | ] 60 | {% endif %} 61 | {% if resourceCount.image > 0 %} 62 | {{accessModifier}} static let allImages: [{{imageType}}] = [ 63 | {% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %} 64 | ] 65 | {% endif %} 66 | {% if resourceCount.symbol > 0 %} 67 | {{accessModifier}} static let allSymbols: [{{symbolType}}] = [ 68 | {% filter indent:2 %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %} 69 | ] 70 | {% endif %} 71 | // swiftlint:enable trailing_comma 72 | {% endif %} 73 | {% endmacro %} 74 | {% macro casesBlock assets %} 75 | {% for asset in assets %} 76 | {% if asset.type == "arresourcegroup" %} 77 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}") 78 | {% elif asset.type == "color" %} 79 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}") 80 | {% elif asset.type == "data" %} 81 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}") 82 | {% elif asset.type == "image" %} 83 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}") 84 | {% elif asset.type == "symbol" %} 85 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}") 86 | {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %} 87 | {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 88 | {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %} 89 | } 90 | {% elif asset.items %} 91 | {% call casesBlock asset.items %} 92 | {% endif %} 93 | {% endfor %} 94 | {% endmacro %} 95 | {% macro allValuesBlock assets filter prefix %} 96 | {% for asset in assets %} 97 | {% if asset.type == filter %} 98 | {{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}, 99 | {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %} 100 | {% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %} 101 | {% call allValuesBlock asset.items filter prefix2 %} 102 | {% elif asset.items %} 103 | {% call allValuesBlock asset.items filter prefix %} 104 | {% endif %} 105 | {% endfor %} 106 | {% endmacro %} 107 | {% macro allUIColorsBlock assets %} 108 | {% for asset in assets %} 109 | {% if asset.type == "color" %} 110 | static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = UIColor(named: "{{asset.value}}", in: BundleToken.bundle, compatibleWith: nil) 111 | {% elif asset.items %} 112 | {% call allUIColorsBlock asset.items %} 113 | {% endif %} 114 | {% endfor %} 115 | {% endmacro %} 116 | {% macro allSwiftUIColorsBlock assets %} 117 | {% for asset in assets %} 118 | {% if asset.type == "color" %} 119 | static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = SwiftUI.Color("{{asset.value}}", bundle: BundleToken.bundle) 120 | {% elif asset.items %} 121 | {% call allSwiftUIColorsBlock asset.items %} 122 | {% endif %} 123 | {% endfor %} 124 | {% endmacro %} 125 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 126 | {{accessModifier}} enum {{enumName}} { 127 | {% if catalogs.count > 1 or param.forceFileNameEnum %} 128 | {% for catalog in catalogs %} 129 | {{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 130 | {% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %} 131 | } 132 | {% endfor %} 133 | {% else %} 134 | {% call enumBlock catalogs.first.assets %} 135 | {% endif %} 136 | } 137 | 138 | {{accessModifier}} extension UIColor { 139 | {% call allUIColorsBlock catalogs.first.assets %} 140 | } 141 | 142 | {{accessModifier}} extension SwiftUI.Color { 143 | {% call allSwiftUIColorsBlock catalogs.first.assets %} 144 | } 145 | 146 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 147 | 148 | // MARK: - Implementation Details 149 | {% if resourceCount.arresourcegroup > 0 %} 150 | 151 | {{accessModifier}} struct {{arResourceGroupType}} { 152 | {{accessModifier}} fileprivate(set) var name: String 153 | 154 | #if os(iOS) 155 | @available(iOS 11.3, *) 156 | {{accessModifier}} var referenceImages: Set { 157 | return ARReferenceImage.referenceImages(in: self) 158 | } 159 | 160 | @available(iOS 12.0, *) 161 | {{accessModifier}} var referenceObjects: Set { 162 | return ARReferenceObject.referenceObjects(in: self) 163 | } 164 | #endif 165 | } 166 | 167 | #if os(iOS) 168 | @available(iOS 11.3, *) 169 | {{accessModifier}} extension ARReferenceImage { 170 | static func referenceImages(in asset: {{arResourceGroupType}}) -> Set { 171 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 172 | return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set() 173 | } 174 | } 175 | 176 | @available(iOS 12.0, *) 177 | {{accessModifier}} extension ARReferenceObject { 178 | static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set { 179 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 180 | return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set() 181 | } 182 | } 183 | #endif 184 | {% endif %} 185 | {% if resourceCount.color > 0 %} 186 | 187 | {{accessModifier}} final class {{colorType}} { 188 | {{accessModifier}} fileprivate(set) var name: String 189 | 190 | #if os(macOS) 191 | {{accessModifier}} typealias Color = NSColor 192 | #elseif os(iOS) || os(tvOS) || os(watchOS) 193 | {{accessModifier}} typealias Color = UIColor 194 | #endif 195 | 196 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 197 | {{accessModifier}} private(set) lazy var color: Color = { 198 | guard let color = Color(asset: self) else { 199 | fatalError("Unable to load color asset named \(name).") 200 | } 201 | return color 202 | }() 203 | 204 | #if os(iOS) || os(tvOS) 205 | @available(iOS 11.0, tvOS 11.0, *) 206 | {{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color { 207 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 208 | guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { 209 | fatalError("Unable to load color asset named \(name).") 210 | } 211 | return color 212 | } 213 | #endif 214 | 215 | fileprivate init(name: String) { 216 | self.name = name 217 | } 218 | } 219 | 220 | {{accessModifier}} extension {{colorType}}.Color { 221 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 222 | convenience init?(asset: {{colorType}}) { 223 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 224 | #if os(iOS) || os(tvOS) 225 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 226 | #elseif os(macOS) 227 | self.init(named: NSColor.Name(asset.name), bundle: bundle) 228 | #elseif os(watchOS) 229 | self.init(named: asset.name) 230 | #endif 231 | } 232 | } 233 | {% endif %} 234 | {% if resourceCount.data > 0 %} 235 | 236 | {{accessModifier}} struct {{dataType}} { 237 | {{accessModifier}} fileprivate(set) var name: String 238 | 239 | @available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *) 240 | {{accessModifier}} var data: NSDataAsset { 241 | guard let data = NSDataAsset(asset: self) else { 242 | fatalError("Unable to load data asset named \(name).") 243 | } 244 | return data 245 | } 246 | } 247 | 248 | @available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *) 249 | {{accessModifier}} extension NSDataAsset { 250 | convenience init?(asset: {{dataType}}) { 251 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 252 | #if os(iOS) || os(tvOS) || os(watchOS) 253 | self.init(name: asset.name, bundle: bundle) 254 | #elseif os(macOS) 255 | self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) 256 | #endif 257 | } 258 | } 259 | {% endif %} 260 | {% if resourceCount.image > 0 %} 261 | 262 | {{accessModifier}} struct {{imageType}} { 263 | {{accessModifier}} fileprivate(set) var name: String 264 | 265 | #if os(macOS) 266 | {{accessModifier}} typealias Image = NSImage 267 | #elseif os(iOS) || os(tvOS) || os(watchOS) 268 | {{accessModifier}} typealias Image = UIImage 269 | #endif 270 | 271 | @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) 272 | {{accessModifier}} var image: Image { 273 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 274 | #if os(iOS) || os(tvOS) 275 | let image = Image(named: name, in: bundle, compatibleWith: nil) 276 | #elseif os(macOS) 277 | let name = NSImage.Name(self.name) 278 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) 279 | #elseif os(watchOS) 280 | let image = Image(named: name) 281 | #endif 282 | guard let result = image else { 283 | fatalError("Unable to load image asset named \(name).") 284 | } 285 | return result 286 | } 287 | 288 | #if os(iOS) || os(tvOS) 289 | @available(iOS 8.0, tvOS 9.0, *) 290 | {{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image { 291 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 292 | guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { 293 | fatalError("Unable to load image asset named \(name).") 294 | } 295 | return result 296 | } 297 | #endif 298 | } 299 | 300 | {{accessModifier}} extension {{imageType}}.Image { 301 | @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) 302 | @available(macOS, deprecated, 303 | message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property") 304 | convenience init?(asset: {{imageType}}) { 305 | #if os(iOS) || os(tvOS) 306 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 307 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 308 | #elseif os(macOS) 309 | self.init(named: NSImage.Name(asset.name)) 310 | #elseif os(watchOS) 311 | self.init(named: asset.name) 312 | #endif 313 | } 314 | } 315 | {% endif %} 316 | {% if resourceCount.symbol > 0 %} 317 | 318 | {{accessModifier}} struct {{symbolType}} { 319 | {{accessModifier}} fileprivate(set) var name: String 320 | 321 | #if os(iOS) || os(tvOS) || os(watchOS) 322 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) 323 | {{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration 324 | {{accessModifier}} typealias Image = UIImage 325 | 326 | @available(iOS 12.0, tvOS 12.0, watchOS 5.0, *) 327 | {{accessModifier}} var image: Image { 328 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 329 | #if os(iOS) || os(tvOS) 330 | let image = Image(named: name, in: bundle, compatibleWith: nil) 331 | #elseif os(watchOS) 332 | let image = Image(named: name) 333 | #endif 334 | guard let result = image else { 335 | fatalError("Unable to load symbol asset named \(name).") 336 | } 337 | return result 338 | } 339 | 340 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) 341 | {{accessModifier}} func image(with configuration: Configuration) -> Image { 342 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 343 | guard let result = Image(named: name, in: bundle, with: configuration) else { 344 | fatalError("Unable to load symbol asset named \(name).") 345 | } 346 | return result 347 | } 348 | #endif 349 | } 350 | {% endif %} 351 | {% if not param.bundle %} 352 | 353 | // swiftlint:disable convenience_type 354 | private final class BundleToken { 355 | static let bundle: Bundle = { 356 | #if SWIFT_PACKAGE 357 | return Bundle.module 358 | #else 359 | return Bundle(for: BundleToken.self) 360 | #endif 361 | }() 362 | } 363 | // swiftlint:enable convenience_type 364 | {% endif %} 365 | {% else %} 366 | // No assets found 367 | {% endif %} -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/texterify.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_base_url": "https://texterify.allaboutapps.at/api", 3 | "api_version": "v1", 4 | "project_id": "{{cookiecutter.texterifyProjectId}}", 5 | "export_configuration_id": "{{cookiecutter.texterifyExportConfigurationId}}", 6 | "export_directory": "Modules/Core/Sources/Assets/Resources", 7 | "project_path": "" 8 | } 9 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Assets/Assets.xcassets/AppIcon.appiconset/02 orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allaboutapps/ios-starter/74d87349b1f51eaa4bc6241735b12bd2eda5ad4c/{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Assets/Assets.xcassets/AppIcon.appiconset/02 orange.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "02 orange.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Code/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | import AuthFeature 2 | import Combine 3 | import ForceUpdate 4 | import MainFeature 5 | import Networking 6 | import Toolbox 7 | import UIKit 8 | import Utilities 9 | 10 | class AppCoordinator: Coordinator { 11 | // MARK: Init 12 | 13 | override private init(rootViewController: UIViewController) { 14 | super.init(rootViewController: rootViewController) 15 | } 16 | 17 | // MARK: Properties 18 | 19 | static let shared = AppCoordinator(rootViewController: .init()) 20 | 21 | private(set) var window: UIWindow! 22 | private var cancellables = Set() 23 | private let mainCoordinator = MainCoordinator(tabBarController: .init()) 24 | private var forceUpdateWindow: ForceUpdateWindow? 25 | 26 | // MARK: Start 27 | 28 | func start(window: UIWindow) { 29 | self.window = window 30 | 31 | mainCoordinator.start() 32 | addChild(mainCoordinator) 33 | 34 | window.rootViewController = mainCoordinator.rootViewController 35 | window.makeKeyAndVisible() 36 | 37 | printRootDebugStructure() 38 | 39 | CredentialsController.shared.currentCredentialsDidChange 40 | .sink { [weak self] credentials in 41 | if credentials == nil { 42 | self?.presentLogin(animated: true) 43 | } 44 | } 45 | .store(in: &cancellables) 46 | 47 | if CredentialsController.shared.currentCredentials == nil { 48 | presentLogin(animated: false) 49 | } 50 | 51 | if AppEnvironment.current.buildConfig != .debug { 52 | let updateController = Services.shared[ForceUpdateController.self] 53 | Task { 54 | for await url in updateController.onForceUpdateNeededAsyncSequence { 55 | self.presentForceUpdate(url: url) 56 | } 57 | } 58 | } 59 | } 60 | 61 | // MARK: Present 62 | 63 | private func presentLogin(animated: Bool) { 64 | let coordinator = AuthCoordinator(navigationController: .init()) 65 | 66 | coordinator.onLogin = { [weak self] in 67 | self?.reset(animated: true) 68 | } 69 | 70 | coordinator.start() 71 | 72 | addChild(coordinator) 73 | window.topViewController()?.present(coordinator.rootViewController, animated: animated, completion: nil) 74 | } 75 | 76 | private func presentForceUpdate(url: URL?) { 77 | guard forceUpdateWindow == nil else { return } 78 | let appearance = ForceUpdateAppearance( 79 | imageForegroundColor: .green, 80 | toAppStoreButtonTintColor: .green 81 | ) 82 | forceUpdateWindow = ForceUpdateWindow(appStoreURL: url, appearance: appearance) 83 | forceUpdateWindow?.show() 84 | } 85 | 86 | // MARK: Helpers 87 | 88 | func reset(animated: Bool) { 89 | childCoordinators 90 | .filter { $0 !== mainCoordinator } 91 | .forEach { removeChild($0) } 92 | 93 | mainCoordinator.reset(animated: animated) 94 | 95 | printRootDebugStructure() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Code/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import CommonUI 2 | import DebugView 3 | import ForceUpdate 4 | import Foundation 5 | import Logbook 6 | import Networking 7 | import Toolbox 8 | import UIKit 9 | import Utilities 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | setupLogging(for: AppEnvironment.current) 17 | log.info(AppEnvironment.current.appInfo) 18 | 19 | Appearance.setup() 20 | API.setup() 21 | CredentialsController.shared.resetOnNewInstallations() 22 | 23 | setupDebug() 24 | setupForceUpdate() 25 | 26 | return true 27 | } 28 | 29 | // MARK: UISceneSession Lifecycle 30 | 31 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { 32 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 33 | } 34 | } 35 | 36 | // MARK: - Logging 37 | 38 | extension AppDelegate { 39 | func setupLogging(for environment: AppEnvironment) { 40 | let dateformatter = DateFormatter() 41 | dateformatter.dateStyle = .none 42 | dateformatter.timeStyle = .medium 43 | 44 | switch environment.buildConfig { 45 | case .debug: 46 | let sink = ConsoleLogSink(level: .min(.debug)) 47 | 48 | sink.format = "> \(LogPlaceholder.category) \(LogPlaceholder.date): \(LogPlaceholder.messages)" 49 | sink.dateFormatter = dateformatter 50 | 51 | log.add(sink: sink) 52 | case .release: 53 | log.add(sink: OSLogSink(level: .min(.warning))) 54 | } 55 | } 56 | } 57 | 58 | // MARK: - Force Update 59 | 60 | private extension AppDelegate { 61 | /// Sets up the `ForceUpdateController` and calls `checkForUpdate()`, if not in debug mode. 62 | func setupForceUpdate() { 63 | guard AppEnvironment.current.buildConfig != .debug else { return } 64 | let updateController = ForceUpdateController( 65 | publicVersionURL: Config.ForceUpdate.publicVersionURL 66 | ) 67 | 68 | Services.shared.register(service: updateController) 69 | 70 | Task { 71 | await updateController.checkForUpdate() 72 | } 73 | } 74 | } 75 | 76 | // MARK: - Debug 77 | 78 | private extension AppDelegate { 79 | /// Sets up the `DebugController` and sets values, if enabled in `Config.Debug.enabled`. 80 | func setupDebug() { 81 | guard Config.Debug.enabled else { return } 82 | 83 | let debugController = DebugController() 84 | 85 | debugController.addValue(.appVersion, toSection: .app) 86 | debugController.addValue(.appBuildNumber, toSection: .app) 87 | debugController.addValue(.appBundleIdentifier, toSection: .app) 88 | debugController.addValue(.serverEnvironment, toSection: .app) 89 | debugController.addValue(.buildConfig, toSection: .app) 90 | 91 | debugController.addValue(.appStart, toSection: .user) 92 | debugController.addValue(.userLocale, toSection: .user) 93 | 94 | debugController.addValue(.deviceOSVersion, toSection: .device) 95 | debugController.addValue(.deviceOSModel, toSection: .device) 96 | 97 | debugController.addValue(.pushNotificationsToken, toSection: .pushNotifications) 98 | debugController.addValue(.pushNotificationsEnvironment, toSection: .pushNotifications) 99 | debugController.addValue(.pushNotificationsRegistered, toSection: .pushNotifications) 100 | 101 | debugController.addButton( 102 | DebugButton( 103 | id: "diceRoll", 104 | label: "Copy Dice Roll", 105 | action: { 106 | UIPasteboard.general.string = String(Int.random(in: 1 ... 6)) 107 | } 108 | ), 109 | toSection: .user 110 | ) 111 | 112 | Services.shared.register(service: debugController) 113 | } 114 | } 115 | 116 | private extension DebugValue { 117 | static let pushNotificationsEnvironment = DebugValue( 118 | id: "pushNotificationsEnvironment", 119 | label: "Environment", 120 | staticValue: AppEnvironment.current.buildConfig == .debug ? "development" : "production" 121 | ) 122 | 123 | static let pushNotificationsToken = DebugValue( 124 | id: "pushNotificationsToken", 125 | label: "Token", 126 | value: "ABC" 127 | ) 128 | 129 | static let appStart = DebugValue( 130 | id: "appStart", 131 | label: "App Start", 132 | staticValue: Date.now.formatted(date: .complete, time: .complete) 133 | ) 134 | 135 | static let serverEnvironment = DebugValue( 136 | id: "serverEnvironment", 137 | label: "Server Environment", 138 | staticValue: AppEnvironment.current.serverEnvironment.rawValue 139 | ) 140 | 141 | static let buildConfig = DebugValue( 142 | id: "buildConfig", 143 | label: "Build Config", 144 | staticValue: AppEnvironment.current.buildConfig.rawValue 145 | ) 146 | } 147 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/Code/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import ForceUpdate 2 | import Toolbox 3 | import UIKit 4 | import Utilities 5 | 6 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 7 | var window: UIWindow? 8 | 9 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 10 | guard let windowScene = (scene as? UIWindowScene) else { return } 11 | 12 | let window = UIWindow(windowScene: windowScene) 13 | 14 | AppCoordinator.shared.start(window: window) 15 | 16 | self.window = window 17 | window.makeKeyAndVisible() 18 | } 19 | 20 | func sceneDidDisconnect(_: UIScene) {} 21 | 22 | func sceneDidBecomeActive(_: UIScene) {} 23 | 24 | func sceneWillResignActive(_: UIScene) {} 25 | 26 | func sceneWillEnterForeground(_: UIScene) { 27 | guard AppEnvironment.current.buildConfig != .debug else { return } 28 | let updateController = Services.shared[ForceUpdateController.self] 29 | 30 | Task { 31 | await updateController.checkForUpdate() 32 | } 33 | } 34 | 35 | func sceneDidEnterBackground(_: UIScene) {} 36 | } 37 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/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 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Configurations/Dev.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}}-dev 2 | SERVER_ENVIRONMENT = dev 3 | SERVER_ENVIRONMENT_SUFFIX = -dev 4 | APP_NAME_SUFFIX = -Dev -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Configurations/Live.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}} 2 | SERVER_ENVIRONMENT = live 3 | SERVER_ENVIRONMENT_SUFFIX = 4 | APP_NAME_SUFFIX = -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Configurations/Staging.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}}-staging 2 | SERVER_ENVIRONMENT = staging 3 | SERVER_ENVIRONMENT_SUFFIX = -staging 4 | APP_NAME_SUFFIX = -Staging -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | {{cookiecutter.projectName}}$(APP_NAME_SUFFIX) 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 | 1.0 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | ITSAppUsesNonExemptEncryption 24 | 25 | LSRequiresIPhoneOS 26 | 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | UISceneConfigurations 32 | 33 | UIWindowSceneSessionRoleApplication 34 | 35 | 36 | UISceneConfigurationName 37 | Default Configuration 38 | UISceneDelegateClassName 39 | $(PRODUCT_MODULE_NAME).SceneDelegate 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | 54 | _Configuration 55 | $(CONFIGURATION) 56 | _ServerEnvironment 57 | $(SERVER_ENVIRONMENT) 58 | 59 | 60 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | File 9 | com.mono0926.LicensePlist 10 | Title 11 | Licenses 12 | Type 13 | PSChildPaneSpecifier 14 | 15 | 16 | Title 17 | 18 | Type 19 | PSGroupSpecifier 20 | 21 | 22 | DefaultValue 23 | 1.0.0 (1) 24 | Key 25 | AppVersion 26 | Title 27 | Version 28 | Type 29 | PSTitleValueSpecifier 30 | 31 | 32 | StringsTable 33 | Root 34 | 35 | 36 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt: -------------------------------------------------------------------------------- 1 | name: Alamofire, nameSpecified: Alamofire, owner: Alamofire, version: 5.7.1, source: https://github.com/Alamofire/Alamofire 2 | 3 | name: AlamofireImage, nameSpecified: AlamofireImage, owner: Alamofire, version: 4.2.0, source: https://github.com/Alamofire/AlamofireImage 4 | 5 | name: DataSource, nameSpecified: DataSource, owner: allaboutapps, version: 8.1.4, source: https://github.com/allaboutapps/DataSource 6 | 7 | name: Differ, nameSpecified: Differ, owner: tonyarnold, version: 1.4.6, source: https://github.com/tonyarnold/Differ 8 | 9 | name: Fetch, nameSpecified: Fetch, owner: allaboutapps, version: 3.1.5, source: https://github.com/allaboutapps/Fetch 10 | 11 | name: KeychainAccess, nameSpecified: KeychainAccess, owner: kishikawakatsumi, version: 4.2.2, source: https://github.com/kishikawakatsumi/KeychainAccess 12 | 13 | name: Logbook, nameSpecified: Logbook, owner: allaboutapps, version: 1.2.1, source: https://github.com/allaboutapps/Logbook 14 | 15 | name: StatefulViewController, nameSpecified: StatefulViewController, owner: allaboutapps, version: 5.2.0, source: https://github.com/allaboutapps/StatefulViewController 16 | 17 | name: Toolbox, nameSpecified: Toolbox, owner: allaboutapps, version: 3.0.1, source: https://github.com/allaboutapps/Toolbox 18 | 19 | name: Alamofire, nameSpecified: Alamofire, owner: Alamofire, version: 5.7.1, source: https://github.com/Alamofire/Alamofire 20 | 21 | name: AlamofireImage, nameSpecified: AlamofireImage, owner: Alamofire, version: 4.2.0, source: https://github.com/Alamofire/AlamofireImage 22 | 23 | name: DataSource, nameSpecified: DataSource, owner: allaboutapps, version: 8.1.4, source: https://github.com/allaboutapps/DataSource 24 | 25 | name: Differ, nameSpecified: Differ, owner: tonyarnold, version: 1.4.6, source: https://github.com/tonyarnold/Differ 26 | 27 | name: Fetch, nameSpecified: Fetch, owner: allaboutapps, version: 3.1.5, source: https://github.com/allaboutapps/Fetch 28 | 29 | name: KeychainAccess, nameSpecified: KeychainAccess, owner: kishikawakatsumi, version: 4.2.2, source: https://github.com/kishikawakatsumi/KeychainAccess 30 | 31 | name: Logbook, nameSpecified: Logbook, owner: allaboutapps, version: 1.2.1, source: https://github.com/allaboutapps/Logbook 32 | 33 | name: StatefulViewController, nameSpecified: StatefulViewController, owner: allaboutapps, version: 5.2.0, source: https://github.com/allaboutapps/StatefulViewController 34 | 35 | name: Toolbox, nameSpecified: Toolbox, owner: allaboutapps, version: 3.0.1, source: https://github.com/allaboutapps/Toolbox 36 | 37 | add-version-numbers: false 38 | 39 | LicensePlist Version: 3.24.10 -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | Title 9 | Licenses 10 | Type 11 | PSGroupSpecifier 12 | 13 | 14 | File 15 | com.mono0926.LicensePlist/Alamofire 16 | Title 17 | Alamofire 18 | Type 19 | PSChildPaneSpecifier 20 | 21 | 22 | File 23 | com.mono0926.LicensePlist/AlamofireImage 24 | Title 25 | AlamofireImage 26 | Type 27 | PSChildPaneSpecifier 28 | 29 | 30 | File 31 | com.mono0926.LicensePlist/DataSource 32 | Title 33 | DataSource 34 | Type 35 | PSChildPaneSpecifier 36 | 37 | 38 | File 39 | com.mono0926.LicensePlist/Differ 40 | Title 41 | Differ 42 | Type 43 | PSChildPaneSpecifier 44 | 45 | 46 | File 47 | com.mono0926.LicensePlist/Fetch 48 | Title 49 | Fetch 50 | Type 51 | PSChildPaneSpecifier 52 | 53 | 54 | File 55 | com.mono0926.LicensePlist/KeychainAccess 56 | Title 57 | KeychainAccess 58 | Type 59 | PSChildPaneSpecifier 60 | 61 | 62 | File 63 | com.mono0926.LicensePlist/Logbook 64 | Title 65 | Logbook 66 | Type 67 | PSChildPaneSpecifier 68 | 69 | 70 | File 71 | com.mono0926.LicensePlist/StatefulViewController 72 | Title 73 | StatefulViewController 74 | Type 75 | PSChildPaneSpecifier 76 | 77 | 78 | File 79 | com.mono0926.LicensePlist/Toolbox 80 | Title 81 | Toolbox 82 | Type 83 | PSChildPaneSpecifier 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/Alamofire.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | 29 | License 30 | MIT 31 | Type 32 | PSGroupSpecifier 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/AlamofireImage.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Copyright (c) 2015-2021 Alamofire Software Foundation (http://alamofire.org/) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | 29 | License 30 | MIT 31 | Type 32 | PSGroupSpecifier 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/DataSource.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2016 - 2023 all about apps GmbH 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | License 32 | MIT 33 | Type 34 | PSGroupSpecifier 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/Differ.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2017 Tony Arnold 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | License 20 | MIT 21 | Type 22 | PSGroupSpecifier 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/Fetch.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2019 all about apps GmbH 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | License 32 | MIT 33 | Type 34 | PSGroupSpecifier 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/KeychainAccess.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2014 kishikawa katsumi 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | 32 | License 33 | MIT 34 | Type 35 | PSGroupSpecifier 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/Logbook.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | MIT License 10 | 11 | Copyright (c) 2019 aaa - all about apps GmbH 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | 31 | License 32 | MIT 33 | Type 34 | PSGroupSpecifier 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/StatefulViewController.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2014 - 2017 Alexander Schuch (http://schuch.me) 12 | Copyright (c) 2018 - 2019 all about apps GmbH 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | THE SOFTWARE. 31 | 32 | License 33 | MIT 34 | Type 35 | PSGroupSpecifier 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/{{cookiecutter.projectName}}/SupportingFiles/Settings.bundle/com.mono0926.LicensePlist/Toolbox.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2016 - 2023 all about apps GmbH 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 | THE SOFTWARE. 30 | 31 | License 32 | MIT 33 | Type 34 | PSGroupSpecifier 35 | 36 | 37 | 38 | 39 | --------------------------------------------------------------------------------