├── .gitignore ├── CHANGELOG.md ├── README.md ├── cookiecutter.json ├── hooks ├── post_gen_project.sh └── pre_gen_project.sh └── {{cookiecutter.projectDirectory}} ├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .githooks ├── commit-msg └── verify_conventional_commit ├── .gitignore ├── .swift-version ├── .swiftformat ├── .tuist-version ├── CHANGELOG.md ├── Gemfile ├── Mintfile ├── Project.swift ├── README.md ├── Scripts └── SwiftFormatRunScript.sh ├── Setup.swift ├── Targets ├── iOS │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon-ios-20@2x.png │ │ │ ├── icon-ios-20@3x.png │ │ │ ├── icon-ios-29@2x.png │ │ │ ├── icon-ios-29@3x.png │ │ │ ├── icon-ios-40@2x.png │ │ │ ├── icon-ios-40@3x.png │ │ │ ├── icon-ios-60@2x.png │ │ │ └── icon-ios-60@3x.png │ │ │ └── Contents.json │ ├── Sources │ │ ├── AppDelegate.swift │ │ ├── Helpers │ │ │ └── Configuration.swift │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ ├── SupportingFiles │ │ └── Config │ │ │ ├── Configuration.xcconfig │ │ │ ├── Development.xcconfig │ │ │ ├── Production.xcconfig │ │ │ └── Staging.xcconfig │ └── Tests │ │ ├── UITests │ │ ├── SupportingFiles │ │ │ └── Info.plist │ │ └── {{cookiecutter.projectName|replace(' ', '_')}}UITests.swift │ │ └── UnitTests │ │ ├── SupportingFiles │ │ └── Info.plist │ │ └── {{cookiecutter.projectName|replace(' ', '_')}}Tests.swift └── {{cookiecutter.projectName}}Kit │ ├── Sources │ ├── {{cookiecutter.projectName}}Kit.h │ └── {{cookiecutter.projectName}}Kit.swift │ ├── SupportingFiles │ └── .gitkeep │ └── Tests │ ├── SupportingFiles │ └── Info.plist │ └── {{cookiecutter.projectName}}KitTests.swift ├── Tuist ├── Config.swift └── ProjectDescriptionHelpers │ └── Project+Templates.swift └── fastlane ├── Fastfile ├── README.md └── actions ├── create_changelog.rb └── crowdin.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2020-12-06 9 | 10 | ### Added 11 | 12 | * `tuist` - Offers to configure your project in Swift which is pretty nice. Therefore this template uses `tuist` now 13 | * `swiftformat` - Rationale explained in "Removed" section 14 | 15 | ### Removed 16 | * `R.swift` - After thinking about it, adding a library that generates type safe assets was too opinionated for a template project, therefore I removed it 17 | * `SwiftLint` - `SwiftLint` was removed in favour of `swiftformat` as it offers pretty much the same functionality than `SwiftLint` but it could potentially also format source code 18 | 19 | ## [0.5.1] - 2020-05-12 20 | 21 | ### Fixed 22 | 23 | - Fixed import issue on LoggingKit and log category keypaths 24 | 25 | ## [0.5.0] - 2020-05-06 26 | 27 | This release removes several files which were a little bit too opinionated. So for example I removed the protocols which were tailored to use the coordinator pattern. I think it's better to keep a template open for every kind of architectural pattern. 28 | Additionally I removed third party dependencies which I personally don't use anymore and I also think they were too opinionated as well. 29 | 30 | ### Added 31 | 32 | - Separate test target for the `Kit` module 33 | - [LoggingKit](https://github.com/alexanderwe/LoggingKit) micro framework 34 | 35 | ### Changed 36 | 37 | - More detailed README 38 | - Updated dependencies 39 | 40 | ### Removed 41 | 42 | - Several external libraries 43 | - PromiseKit 44 | - SwiftDate 45 | - IQKeyboardManager 46 | - Protocols 47 | - Label views 48 | 49 | ## [0.4.0] - 2019-11-16 50 | 51 | ### Added 52 | 53 | - Add Fastlane 54 | - Add Mint for build tool dependencies 55 | 56 | ### Changed: 57 | 58 | - Switch to Swift Package Manager and therefore Xcode 11 59 | - Add support for SceneDelegate on iOS 13 and additionally SwiftUI 60 | - Different build configurations 61 | 62 | ## [0.0.1] - 2019-08-16 63 | 64 | ### Added 65 | 66 | - External libraries 67 | - SwiftLint 68 | - R.Swift 69 | - SwiftDate 70 | - IQKeyboardManager 71 | - PromiseKit 72 | - Internals 73 | - Utilities 74 | - Logging 75 | - Localisation 76 | - UserDefaults 77 | 78 | ### Fixed 79 | 80 | - Errors in `project.yml` which lead to incorrect configured Xcode projects 81 | 82 | [unreleased]: https://github.com/alexanderwe/ios-starter/compare/v1.0.0...HEAD 83 | [0.0.1]: https://github.com/alexanderwe/ios-starter/releases/tag/v0.0.1 84 | [0.5.0]: https://github.com/alexanderwe/ios-starter/releases/tag/v0.5.0 85 | [0.5.1]: https://github.com/alexanderwe/ios-starter/releases/tag/v0.5.1 86 | [1.0.0]: https://github.com/alexanderwe/ios-starter/releases/tag/v1.0.0 87 | 88 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS-Starter 2 | 3 | ![version](https://img.shields.io/badge/version-v1.0.0-green) 4 | 5 | This is a template project for boostrapping iOS applications 6 | 7 | ## Prequisites 8 | 9 | Install [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/index.html),[tuist](https://github.com/tuist/tuist) and [Mint](https://github.com/yonaskolb/Mint) in order to create a new project from this template: 10 | 11 | ```sh 12 | brew install cookiecutter 13 | bash <(curl -Ls https://install.tuist.io) 14 | brew install mint 15 | ``` 16 | 17 | ## Use it 18 | 19 | ```sh 20 | cookiecutter https://github.com/alexanderwe/ios-starter.git 21 | ``` 22 | 23 | - `cookiecutter` will prompt you for: 24 | - The project name 25 | - Apple Developer Team details 26 | - other details necessary for the project 27 | - `cookiecutter` will create all necessary files 28 | - `tuist` runs and automatically creates a `.xcodeproj` file 29 | - Afterwards `mint bootstratp` will run to download all necessary build tools 30 | - Finally `Xcode` will launch your new project 31 | - All code dependencies are managed with the `Swift Package Manager` 32 | - Happy Coding ! 33 | 34 | ## Included external libraries 35 | 36 | - Logging Helper 37 | - [LoggingKit](https://github.com/alexanderwe/LoggingKit) - A small micro-framework for logging 38 | - Code Style 39 | - [swiftformat](https://github.com/nicklockwood/SwiftFormat) - Format and lint your source code 40 | 41 | 42 | ## Structure 43 | 44 | The structure of the template project tries to follow the idea of having a directory for each of your target. In each of these targets you can find it sources and the corresponding tests. 45 | 46 | External dependencies are managed via `Swift Package Manager`. Therefore it is mandatory to use **Xcode 11**. 47 | 48 | ### External build tools 49 | 50 | External build tools like `swiftformat` are managed by `Mint`. 51 | 52 | ### Modules 53 | 54 | The project is divided into several modules: 55 | 56 | - `iOS` - Contains UI code, navigation flows, views if used for SwiftUI or view controllers when UIKit is used 57 | - `Tests iOS` - Unit tests for the iOS application 58 | - `UI Tests iOS` - UI tests for the iOS application 59 | - `Kit` - Source files for your service layer or generally speaking files that could be reused for example for an macOS or watchOS target 60 | - `Kit Tests` - Unit test for the `Kit` target 61 | 62 | ### Schemes 63 | 64 | In general each target has its own scheme but the `iOS` target has three: 65 | 66 | - development 67 | - staging 68 | - production 69 | 70 | Each scheme is used in combination with the files located in `Targets/iOS/SupportingFiles/Config`. There you can find four different files. Three for each scheme and one which contains the values for the active configuration. Each scheme has a `pre-build` action which copies the content from one of "release type" configs to `Configuration.xcconfig` which is then used when the app is running. 71 | 72 | So for example if you are running the `iOS Applcation staging` scheme, the content of the `Staging.xcconfig` file is copied over to `Configuration.xcconfig` and can then be used within the app. 73 | 74 | Currently this mechanism is used to change the app identifier and set some environment variable. This can be extended to for example use different backend urls for different schemes. 75 | 76 | If you want to access one of the variables you declare inside the `.xcconfig` files you need to make them accessible to the app by putting them into the `Derived/InfoPlists/` file. As an example you can have a look at the `_ServerEnvironment` variable which is later accessed within the `ViewController`. 77 | 78 | #### External configuration files 79 | 80 | The mechanism described above can also be used for external configuration files. For example if you have a different `GoogleService-Info.plist` for different builds of your app. Then you can create four different files: 81 | 82 | - GoogleService-Info.development.plist 83 | - GoogleService-Info.staging.plist 84 | - GoogleService-Info.production.plist 85 | - GoogleService-Info.plist 86 | 87 | Inside the `pre-build` actions for the different schemes you copy the content of the corresponding file to `GoogleService-Info.plist` which is then used by your Google frameworks in the app. Such an `pre-build` command could like like the following. 88 | 89 | ```sh 90 | cp -f "${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Firebase/GoogleService-Info.development.plist" "${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Firebase/GoogleService-Info.plist" 91 | ``` 92 | 93 | ### UIKit or SwiftUI 94 | 95 | The projects default UI framework is UIKit. But you can easily opt out and use SwiftUI instead. There is no strict rule to use one or the other. 96 | 97 | ## Git Hooks 98 | 99 | > There is an issue with `GitKraken` not supporting the `config.hooksPath` option. Therefore every hook is copied from the `.githooks` directory to `.git/hooks`. Before that every file that will be changed inside `.git/hooks` is backed up. 100 | 101 | This template comes with a `.githooks` directory. Inside you can find and create hooks which will run in the local `.git` repository. 102 | 103 | ### Available hooks: 104 | 105 | - commit-msg (run before committing): 106 | - Verifies that the commit message follows the standard [Conventional Commits](https://www.conventionalcommits.org) specification 107 | 108 | ## Fastlane 109 | 110 | The template uses [fastlane](https://fastlane.tools) for CI/CD. It comes with a nearly empty `Fastfile` which can be modified as you wish. 111 | Be sure to run `bundle install` first and then execute fastlane through `bundle exec`. 112 | 113 | ```sh 114 | bundle exec fastlane tests 115 | ``` 116 | 117 | ### Included Actions 118 | 119 | - `create_changelog`: Use `git-chglog` and your conventional commits to automatically create a `CHANGELOG.md` in the project directory. `git-chglog` uses the template located in `.chglog` 120 | - `crowdin`: Use the `crowdin` CLI to upload or download your localisation files 121 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "Example", 3 | "projectDirectory": "{{cookiecutter.projectName|lower|replace(' ', '-')}}", 4 | "teamId": "TeamID", 5 | "teamName": "TeamName", 6 | "companyName": "CompanyName", 7 | "bundleIdentifier": "com.example.{{cookiecutter.projectName|lower|replace(' ', '-')}}", 8 | "deploymentTarget": "14.0", 9 | "gitRepository": "https://github.com/alexanderwe/ios-starter.git", 10 | "_copy_without_render": [ 11 | "*.tpl.md" 12 | ] 13 | } -------------------------------------------------------------------------------- /hooks/post_gen_project.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Run tuist to create .xcodeproj file 4 | tuist generate 5 | 6 | # Set up git 7 | git init 8 | 9 | # If tuist has generated a .xcodeproj file we want to open it 10 | xed . 11 | 12 | ## Configure git hooks 13 | chmod +x ./.githooks/commit-msg 14 | git config core.hooksPath .githooks 15 | 16 | ## Patch for GitKraken not using hooks configured via `git config core.hooksPath`, see https://stackoverflow.com/questions/51698712/does-git-kraken-support-global-git-hooks 17 | cp ./.git/hooks/commit-msg.sample ./.git/hooks/commit-msg.sample.backup 18 | cp ./.githooks/commit-msg ./.git/hooks/commit-msg 19 | cp ./.githooks/verify_conventional_commit ./.git/hooks/verify_conventional_commit 20 | 21 | ## Make initial commit 22 | git add . 23 | git commit -m "chore(initial): Initial commit" 24 | 25 | printf 'all done - enjoy your new project 🤓' -------------------------------------------------------------------------------- /hooks/pre_gen_project.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | printf 'We are setting up your new project - hold tight 🧡'  -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Author.Name}} - ({{ .Hash.Short }}): {{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{else}}**Misc:** {{ end }}{{ .Author.Name}} - ({{ .Hash.Short }}): {{ .Subject }} 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .RevertCommits -}} 26 | ### Reverts 27 | {{ range .RevertCommits -}} 28 | - {{ .Revert.Header }} 29 | {{ end }} 30 | {{ end -}} 31 | 32 | {{- if .MergeCommits -}} 33 | ### Pull Requests 34 | {{ range .MergeCommits -}} 35 | - {{ .Header }} 36 | {{ end }} 37 | {{ end -}} 38 | 39 | {{- if .NoteGroups -}} 40 | {{ range .NoteGroups -}} 41 | ### {{ .Title }} 42 | {{ range .Notes }} 43 | {{ .Body }} 44 | {{ end }} 45 | {{ end -}} 46 | {{ end -}} 47 | {{ end -}} 48 | 49 | {{- if .Versions }} 50 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 51 | {{ range .Versions -}} 52 | {{ if .Tag.Previous -}} 53 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 54 | {{ end -}} 55 | {{ end -}} 56 | {{ end -}} -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: {{cookiecutter.gitRepository}} 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | group_by: Type 16 | sort_by: Date 17 | title_maps: 18 | feat: Features 19 | fix: Bug Fixes 20 | perf: Performance Improvements 21 | refactor: Code Refactoring 22 | header: 23 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 24 | pattern_maps: 25 | - Type 26 | - Scope 27 | - Subject 28 | notes: 29 | keywords: 30 | - BREAKING CHANGE -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Directory of the hooks 4 | hookDir=$(dirname "$0") 5 | 6 | # Specify the hooks you want to run during 7 | # the commit-msg process: 8 | "$hookDir"/verify_conventional_commit "$1" -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.githooks/verify_conventional_commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re, sys, os 3 | 4 | def main(): 5 | # examples: 6 | # feat(login): added the ability to log in, into the application 7 | # feat(login/XXX-123): Login button implementation 8 | # fix(XXX-123): Fix bug in login mechanism 9 | # chore: Add file 10 | try: 11 | pattern = r'(feat|fix|docs|style|refactor|perf|test|chore|revert)(\(\S+\))?:\s.*' 12 | filename = sys.argv[1] 13 | ss = open(filename, 'r').read() 14 | m = re.match(pattern, ss) 15 | if m == None: 16 | raise Exception("conventional commit validation failed") 17 | except Exception as e: 18 | print(e) 19 | print("Examples: \nfeat(login): added the ability to log in\nfeat(login/XXX-123): Login button implementation\nfix(XXX-123): Fix bug in login mechanism\nchore: Add file") 20 | exit(1) 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | *.xcodeproj 63 | *.xcworkspace 64 | 65 | ### Tuist derived files ### 66 | graph.dot 67 | Derived/ 68 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.swift-version: -------------------------------------------------------------------------------- 1 | 2 | 5.3 -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.swiftformat: -------------------------------------------------------------------------------- 1 | # format options 2 | --indent tab 3 | --tabwidth 4 4 | 5 | # Enabled rules 6 | --rules duplicateImports 7 | --rules elseOnSameLine 8 | --rules emptyBraces 9 | --rules hoistPatternLet 10 | --rules redundantParens 11 | 12 | 13 | # Disabled rules 14 | --disable indent #Rationale: Until decided, too many false positives 15 | --disable redundantType #Rationale: Sometimes it makes the code easier to understand to explicitly declare the type, even if its obvious for the compiler 16 | --disable redundantReturn #Rationale: Omitting the `return` keyword is a new feature in Swift 5.3. It could be confusing for new developers to omit it 17 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/.tuist-version: -------------------------------------------------------------------------------- 1 | 1.19.0 -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.0.1] - YYYY-MM-DD 11 | 12 | ### Added 13 | 14 | ### Changed 15 | 16 | ### Deprecated 17 | 18 | ### Removed 19 | 20 | ### Fixed 21 | 22 | ### Security 23 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' do 2 | gem 'fastlane', "2.170.0" 3 | end -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Mintfile: -------------------------------------------------------------------------------- 1 | nicklockwood/SwiftFormat@0.47.4 -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | //import ProjectDescriptionHelpers 3 | 4 | let project = Project.init( 5 | name: "{{cookiecutter.projectName}}", 6 | organizationName: "{{cookiecutter.teamName}}", 7 | packages: [ 8 | .package(url: "https://github.com/alexanderwe/LoggingKit", .exact("3.0.0")), 9 | ], 10 | targets: [ 11 | .init(name: "{{cookiecutter.projectName}} (iOS)", 12 | platform: .iOS, 13 | product: .app, 14 | productName: "{{cookiecutter.projectName|replace(' ', '_')}}", 15 | bundleId: "$(PRODUCT_BUNDLE_IDENTIFIER)", 16 | infoPlist: .extendingDefault(with: [ 17 | "CFBundleShortVersionString": .string("0.0.1"), 18 | "CFBundleVersion": .string("1"), 19 | "CFBundleIdentifier": .string("$(PRODUCT_BUNDLE_IDENTIFIER)"), 20 | "CFBundleDisplayName": .string("{{cookiecutter.projectName}}$(SERVER_ENVIRONMENT_SUFFIX)"), 21 | "UIApplicationSceneManifest" : .dictionary([ 22 | "UIApplicationSupportsMultipleScenes": .boolean(true), 23 | "UISceneConfigurations": .dictionary([ 24 | "UIWindowSceneSessionRoleApplication": .array([ 25 | .dictionary([ 26 | "UISceneConfigurationName": "Default Configuration", 27 | "UISceneDelegateClassName":"$(PRODUCT_MODULE_NAME).SceneDelegate" 28 | ]) 29 | ]) 30 | ]) 31 | ]), 32 | "UIApplicationSupportsIndirectInputEvents": .boolean(true), 33 | "UILaunchScreen": .dictionary([:]), 34 | "_ServerEnvironment": .string("$(SERVER_ENVIRONMENT)") 35 | ]), 36 | sources: [ 37 | "Targets/iOS/Sources/**" 38 | ], resources: [ 39 | "Targets/iOS/Resources/**", 40 | "Targets/iOS/SupportingFiles/**" 41 | ], 42 | actions: [ 43 | TargetAction.pre(path: "Scripts/SwiftFormatRunScript.sh", 44 | arguments: "", 45 | name: "Swiftformat (LintMode)") 46 | ], 47 | dependencies: [ 48 | .target(name: "{{cookiecutter.projectName}}Kit") 49 | ], 50 | settings: Settings( 51 | debug: Configuration(xcconfig: "Targets/iOS/SupportingFiles/Config/Configuration.xcconfig"), 52 | release: Configuration(xcconfig: "Targets/iOS/SupportingFiles/Config/Configuration.xcconfig") 53 | ) 54 | ), 55 | .init(name: "Tests iOS", 56 | platform: .iOS, 57 | product: .unitTests, 58 | productName: "Tests_iOS", 59 | bundleId: "{{cookiecutter.bundleIdentifier}}-ios-tests", 60 | infoPlist: .default, 61 | sources: [ 62 | "Targets/iOS/Tests/UnitTests/**" 63 | ], 64 | dependencies: [ 65 | .target(name: "{{cookiecutter.projectName}} (iOS)") 66 | ] 67 | ), 68 | .init(name: "UI Tests iOS", 69 | platform: .iOS, 70 | product: .uiTests, 71 | productName: "UI_Tests_iOS", 72 | bundleId: "{{cookiecutter.bundleIdentifier}}-ios-ui-tests", 73 | infoPlist: .default, 74 | sources: [ 75 | "Targets/iOS/Tests/UITests/**" 76 | ], 77 | dependencies: [ 78 | .target(name: "{{cookiecutter.projectName}} (iOS)") 79 | ] 80 | ), 81 | .init(name: "{{cookiecutter.projectName}}Kit", 82 | platform: .iOS, 83 | product: .framework, 84 | productName: "{{cookiecutter.projectName|replace(' ', '_')}}Kit", 85 | bundleId: "{{cookiecutter.bundleIdentifier}}kit", 86 | infoPlist: .default, 87 | sources: [ 88 | "Targets/{{cookiecutter.projectName}}Kit/Sources/**" 89 | ], 90 | dependencies: [ 91 | .package(product: "LoggingKit") 92 | ] 93 | ), 94 | .init(name: "{{cookiecutter.projectName}}Kit Tests", 95 | platform: .iOS, 96 | product: .unitTests, 97 | productName: "Tests_{{cookiecutter.projectName|replace(' ', '_')}}Kit", 98 | bundleId: "{{cookiecutter.bundleIdentifier}}kit-tests", 99 | infoPlist: .default, 100 | sources: [ 101 | "Targets/{{cookiecutter.projectName}}Kit/Tests/**" 102 | ], 103 | dependencies: [ 104 | .target(name: "{{cookiecutter.projectName}} (iOS)") 105 | ] 106 | ), 107 | ], 108 | schemes: [ 109 | .init(name: "{{cookiecutter.projectName}} (iOS) Development", 110 | shared: true, 111 | buildAction: BuildAction(targets: ["{{cookiecutter.projectName}} (iOS)"], 112 | preActions: [ 113 | .init(scriptText: "cp -f \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Development.xcconfig\" \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Configuration.xcconfig\"", 114 | target: "{{cookiecutter.projectName}} (iOS)" 115 | )] 116 | ), 117 | testAction: TestAction(targets: ["Tests iOS", "UI Tests iOS"]), 118 | runAction: RunAction(executable: "{{cookiecutter.projectName}} (iOS)") 119 | ), 120 | .init(name: "{{cookiecutter.projectName}} (iOS) Staging", 121 | shared: true, 122 | buildAction: BuildAction(targets: ["{{cookiecutter.projectName}} (iOS)"], 123 | preActions: [ 124 | .init(scriptText: "cp -f \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Staging.xcconfig\" \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Configuration.xcconfig\"", 125 | target: "{{cookiecutter.projectName}} (iOS)" 126 | )] 127 | ), 128 | testAction: TestAction(targets: ["Tests iOS", "UI Tests iOS"]), 129 | runAction: RunAction(executable: "{{cookiecutter.projectName}} (iOS)") 130 | ), 131 | .init(name: "{{cookiecutter.projectName}} (iOS) Production", 132 | shared: true, 133 | buildAction: BuildAction(targets: ["{{cookiecutter.projectName}} (iOS)"], 134 | preActions: [ 135 | .init(scriptText: "cp -f \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Production.xcconfig\" \"${PROJECT_DIR}/Targets/iOS/SupportingFiles/Config/Configuration.xcconfig\"", 136 | target: "{{cookiecutter.projectName}} (iOS)" 137 | )] 138 | ), 139 | testAction: TestAction(targets: ["Tests iOS", "UI Tests iOS"]), 140 | runAction: RunAction(executable: "{{cookiecutter.projectName}} (iOS)") 141 | ), 142 | .init(name: "{{cookiecutter.projectName}}Kit", 143 | shared: true, 144 | testAction: TestAction(targets: ["{{cookiecutter.projectName}}Kit Tests"]) 145 | ), 146 | ] 147 | ) 148 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/README.md: -------------------------------------------------------------------------------- 1 | # iOS Project Template 📱 2 | 3 | ## Setup 4 | 5 | - Fill the README 6 | - Initially build the project 7 | - Delete this TODO section. (And enjoy your fresh and clean Project-setup 🙌) 8 | 9 | --- 10 | 11 | # {{cookiecutter.projectName}} 12 | 13 | --- 14 | 15 | ⚡️ Swift: 5.3 📱 iOS {{cookiecutter.deploymentTarget}} 16 | 17 | --- 18 | 19 | Project description in 1-5 sentences. 20 | 21 | [Screenshots] 22 | 23 | ## Structure 24 | 25 | TODO 26 | 27 | ## Documentation 28 | 29 | Where do I find API docs, wireframes, etc.? 30 | 31 | ## CI/CD 32 | 33 | IS Ci/CD set up ? 34 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Scripts/SwiftFormatRunScript.sh: -------------------------------------------------------------------------------- 1 | if mint list | grep -q 'SwiftFormat'; then 2 | mint run swiftformat --lenient --lint "$SRCROOT" 3 | else 4 | echo "error: SwiftFormat not installed; run 'mint bootstrap' to install" 5 | fi -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Setup.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | let setup = Setup([ 3 | .mint() 4 | ]) -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon-ios-20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon-ios-20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon-ios-29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon-ios-29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon-ios-40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon-ios-40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-ios-60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon-ios-60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "20x20", 54 | "scale" : "1x" 55 | }, 56 | { 57 | "idiom" : "ipad", 58 | "size" : "20x20", 59 | "scale" : "2x" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "size" : "29x29", 64 | "scale" : "1x" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "size" : "29x29", 69 | "scale" : "2x" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "size" : "40x40", 74 | "scale" : "1x" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "size" : "40x40", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "idiom" : "ipad", 83 | "size" : "76x76", 84 | "scale" : "1x" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "size" : "76x76", 89 | "scale" : "2x" 90 | }, 91 | { 92 | "idiom" : "ipad", 93 | "size" : "83.5x83.5", 94 | "scale" : "2x" 95 | }, 96 | { 97 | "idiom" : "ios-marketing", 98 | "size" : "1024x1024", 99 | "scale" : "1x" 100 | } 101 | ], 102 | "info" : { 103 | "version" : 1, 104 | "author" : "xcode" 105 | } 106 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // {{cookiecutter.projectName}} 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import {{cookiecutter.projectName}}Kit 11 | import LoggingKit 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | // Override point for customization after application launch. 20 | 21 | // Register log providers for the logging system 22 | LogService.register(logProviders: OSLogProvider()) 23 | 24 | // Verify that target dependency is correctly resolved 25 | {{cookiecutter.projectName}}Kit.hello() 26 | return true 27 | } 28 | 29 | func applicationWillResignActive(_ application: UIApplication) { 30 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 31 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 32 | } 33 | 34 | func applicationDidEnterBackground(_ application: UIApplication) { 35 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 36 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 37 | } 38 | 39 | func applicationWillEnterForeground(_ application: UIApplication) { 40 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 41 | } 42 | 43 | func applicationDidBecomeActive(_ application: UIApplication) { 44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 45 | } 46 | 47 | func applicationWillTerminate(_ application: UIApplication) { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | } 50 | 51 | // MARK: UISceneSession Lifecycle 52 | @available(iOS 13.0, *) 53 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 54 | // Called when a new scene session is being created. 55 | // Use this method to select a configuration to create the new scene with. 56 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 57 | } 58 | 59 | @available(iOS 13.0, *) 60 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 61 | // Called when the user discards a scene session. 62 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 63 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Sources/Helpers/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // {{cookiecutter.projectName}} 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | enum Configuration { 11 | enum Error: Swift.Error { 12 | case missingKey, invalidValue 13 | } 14 | 15 | static func value(for key: String) throws -> T where T: LosslessStringConvertible { 16 | guard let object = Bundle.main.object(forInfoDictionaryKey: key) else { 17 | throw Error.missingKey 18 | } 19 | 20 | switch object { 21 | case let value as T: 22 | return value 23 | case let string as String: 24 | guard let value = T(string) else { throw Error.invalidValue } 25 | return value 26 | default: 27 | throw Error.invalidValue 28 | } 29 | } 30 | } 31 | 32 | // Usage: 33 | // Define your values inside the Info.plist file 34 | // API_BASE_URL: $(BASE_URL) // BASE_URL is the value defined in the .xcconfig files 35 | // 36 | // enum API { 37 | // static var baseURL: URL { 38 | // do { 39 | // let value = try Configuration.value(for: "API_BASE_URL") as String 40 | // return URL(string: "https://" + value)! 41 | // } catch { 42 | // print(error) 43 | // return URL(string: "https://google.com")! 44 | // } 45 | // } 46 | 47 | // static var key: String { 48 | // return try! Configuration.value(for: "API_KEY") 49 | // } 50 | // } 51 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Sources/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // {{cookiecutter.projectName}} 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | //import SwiftUI 11 | 12 | @available(iOS 13.0, *) 13 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | guard let windowScene = (scene as? UIWindowScene) else { return } 22 | 23 | // INFO: If you want to use SwiftUI remove the line above and comment in the following lines 24 | // // Create the SwiftUI view that provides the window contents. 25 | // let contentView = ContentView() 26 | 27 | // // Use a UIHostingController as window root view controller. 28 | // if let windowScene = scene as? UIWindowScene { 29 | // let window = UIWindow(windowScene: windowScene) 30 | // window.rootViewController = UIHostingController(rootView: contentView) 31 | // self.window = window 32 | // window.makeKeyAndVisible() 33 | // } 34 | 35 | //INFO: This is one way of initializing the when using UIKit 36 | 37 | let window = UIWindow(frame: windowScene.coordinateSpace.bounds) 38 | self.window = window 39 | window.windowScene = windowScene 40 | let navigationController = UINavigationController(rootViewController: ViewController()) 41 | window.rootViewController = navigationController 42 | window.makeKeyAndVisible() 43 | } 44 | 45 | func sceneDidDisconnect(_ scene: UIScene) { 46 | // Called as the scene is being released by the system. 47 | // This occurs shortly after the scene enters the background, or when its session is discarded. 48 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 49 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 50 | } 51 | 52 | func sceneDidBecomeActive(_ scene: UIScene) { 53 | // Called when the scene has moved from an inactive state to an active state. 54 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 55 | } 56 | 57 | func sceneWillResignActive(_ scene: UIScene) { 58 | // Called when the scene will move from an active state to an inactive state. 59 | // This may occur due to temporary interruptions (ex. an incoming phone call). 60 | } 61 | 62 | func sceneWillEnterForeground(_ scene: UIScene) { 63 | // Called as the scene transitions from the background to the foreground. 64 | // Use this method to undo the changes made on entering the background. 65 | } 66 | 67 | func sceneDidEnterBackground(_ scene: UIScene) { 68 | // Called as the scene transitions from the foreground to the background. 69 | // Use this method to save data, release shared resources, and store enough scene-specific state information 70 | // to restore the scene back to its current state. 71 | } 72 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // {{cookiecutter.projectName}} 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LoggingKit 11 | 12 | class ViewController: UIViewController { 13 | 14 | 15 | private lazy var label: UILabel = { 16 | let label = UILabel() 17 | label.translatesAutoresizingMaskIntoConstraints = false 18 | label.text = "Hello {{cookiecutter.projectName}}" 19 | return label 20 | 21 | }() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | view.backgroundColor = .systemBackground 26 | view.addSubview(label) 27 | 28 | NSLayoutConstraint.activate([ 29 | label.centerYAnchor.constraint(equalTo: view.centerYAnchor), 30 | label.centerXAnchor.constraint(equalTo: view.centerXAnchor) 31 | ]) 32 | 33 | // Do any additional setup after loading the view. 34 | LogService.shared.info("Welcome to your iOS starter project!", logCategory: \.default) 35 | LogService.shared.info("Current Server environment is \(try? Configuration.value(for: "_ServerEnvironment") as String)", logCategory: \.default) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/SupportingFiles/Config/Configuration.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}} 2 | SERVER_ENVIRONMENT = live 3 | SERVER_ENVIRONMENT_SUFFIX = -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/SupportingFiles/Config/Development.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}}-dev 2 | SERVER_ENVIRONMENT = dev 3 | SERVER_ENVIRONMENT_SUFFIX = -dev -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/SupportingFiles/Config/Production.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}} 2 | SERVER_ENVIRONMENT = live 3 | SERVER_ENVIRONMENT_SUFFIX = -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/SupportingFiles/Config/Staging.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_BUNDLE_IDENTIFIER = {{cookiecutter.bundleIdentifier}}-staging 2 | SERVER_ENVIRONMENT = staging 3 | SERVER_ENVIRONMENT_SUFFIX = -staging -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Tests/UITests/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Tests/UITests/{{cookiecutter.projectName|replace(' ', '_')}}UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // {{cookiecutter.projectName|replace(' ', '_')}}UITests.swift 3 | // {{cookiecutter.projectName}}UITests 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class {{cookiecutter.projectName|replace(' ', '_')}}UITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | 17 | // In UI tests it is usually best to stop immediately when a failure occurs. 18 | continueAfterFailure = false 19 | 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | super.tearDown() 28 | // Put teardown code here. This method is called after the invocation of each test method in the class. 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Tests/UnitTests/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/iOS/Tests/UnitTests/{{cookiecutter.projectName|replace(' ', '_')}}Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // {{cookiecutter.projectName}}Tests.swift 3 | // {{cookiecutter.projectName}}Tests 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import {{cookiecutter.projectName|replace(' ', '_')}} 11 | 12 | class {{cookiecutter.projectName|replace(' ', '_')}}Tests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/Sources/{{cookiecutter.projectName}}Kit.h: -------------------------------------------------------------------------------- 1 | // 2 | // {{cookiecutter.projectName}}Kit.h 3 | // {{cookiecutter.projectName}}Kit 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for {{cookiecutter.projectName}}Kit. 12 | FOUNDATION_EXPORT double {{cookiecutter.projectName}}KitVersionNumber; 13 | 14 | //! Project version string for {{cookiecutter.projectName}}Kit. 15 | FOUNDATION_EXPORT const unsigned char {{cookiecutter.projectName}}KitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/Sources/{{cookiecutter.projectName}}Kit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // {{cookiecutter.projectName}}Kit.swift 3 | // {{cookiecutter.projectName}}Kit 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoggingKit 11 | 12 | public struct {{cookiecutter.projectName}}Kit { 13 | public static func hello() { 14 | LogService.shared.info("Hello from {{cookiecutter.projectName}}Kit", logCategory: \.default) 15 | } 16 | } -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/SupportingFiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexanderwe/ios-starter/3252ffeebfc534e14f2bba0d1c4913bb21a1aeba/{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/SupportingFiles/.gitkeep -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/Tests/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Targets/{{cookiecutter.projectName}}Kit/Tests/{{cookiecutter.projectName}}KitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // {{cookiecutter.projectName}}KitTests.swift 3 | // {{cookiecutter.projectName}}KitTests 4 | // 5 | // Created by Automated on {% now 'utc', '%d/%m/%Y' %}. 6 | // Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import {{cookiecutter.projectName}}Kit 11 | 12 | class {{cookiecutter.projectName}}KitTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | compatibleXcodeVersions: .list(["12.0", "12.0.1", "12.1", "12.2"]), 5 | generationOptions: [ 6 | .disableAutogeneratedSchemes 7 | ] 8 | ) 9 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/Tuist/ProjectDescriptionHelpers/Project+Templates.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | /// Project helpers are functions that simplify the way you define your project. 4 | /// Share code to create targets, settings, dependencies, 5 | /// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks" 6 | /// See https://tuist.io/docs/usage/helpers/ 7 | 8 | extension Project { 9 | /// Helper function to create the Project for this ExampleApp 10 | public static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project { 11 | var targets = makeAppTargets(name: name, 12 | platform: platform, 13 | dependencies: additionalTargets.map { TargetDependency.target(name: $0) }) 14 | targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, platform: platform) }) 15 | return Project(name: name, 16 | organizationName: "tuist.io", 17 | targets: targets) 18 | } 19 | 20 | // MARK: - Private 21 | 22 | /// Helper function to create a framework target and an associated unit test target 23 | private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] { 24 | let sources = Target(name: name, 25 | platform: platform, 26 | product: .framework, 27 | bundleId: "io.tuist.\(name)", 28 | infoPlist: .default, 29 | sources: ["Targets/\(name)/Sources/**"], 30 | resources: [], 31 | dependencies: []) 32 | let tests = Target(name: "\(name)Tests", 33 | platform: platform, 34 | product: .unitTests, 35 | bundleId: "io.tuist.\(name)Tests", 36 | infoPlist: .default, 37 | sources: ["Targets/\(name)/Tests/**"], 38 | resources: [], 39 | dependencies: [.target(name: name)]) 40 | return [sources, tests] 41 | } 42 | 43 | /// Helper function to create the application target and the unit test target. 44 | private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] { 45 | let platform: Platform = platform 46 | let infoPlist: [String: InfoPlist.Value] = [ 47 | "CFBundleShortVersionString": "1.0", 48 | "CFBundleVersion": "1", 49 | "UIMainStoryboardFile": "" 50 | ] 51 | 52 | let mainTarget = Target( 53 | name: name, 54 | platform: platform, 55 | product: .app, 56 | bundleId: "io.tuist.\(name)", 57 | infoPlist: .extendingDefault(with: infoPlist), 58 | sources: ["Targets/\(name)/Sources/**"], 59 | resources: [], 60 | dependencies: dependencies 61 | ) 62 | 63 | let testTarget = Target( 64 | name: "\(name)Tests", 65 | platform: platform, 66 | product: .unitTests, 67 | bundleId: "io.tuist.\(name)Tests", 68 | infoPlist: .default, 69 | sources: ["Targets/\(name)/Tests/**"], 70 | dependencies: [ 71 | .target(name: "\(name)") 72 | ]) 73 | return [mainTarget, testTarget] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # Run tests 2 | lane :tests do 3 | desc "Build and test application" 4 | 5 | run_tests(project: "{{cookiecutter.projectName}}.xcodeproj", 6 | devices: ["iPhone 12 mini"], 7 | scheme: "{{cookiecutter.projectName}} (iOS) Development", 8 | only_testing: ["Tests iOS"] 9 | 10 | ) 11 | run_tests(project: "{{cookiecutter.projectName}}.xcodeproj", 12 | devices: ["iPhone 12 mini"], 13 | scheme: "{{cookiecutter.projectName}}Kit" 14 | ) 15 | end 16 | 17 | # Beta deployment to testflight 18 | lane :beta do 19 | desc "Build, test and upload a new build to TestFlight" 20 | 21 | rescue => exception 22 | on_error(exception) 23 | end 24 | 25 | # Release deployment to App Store 26 | lane :release do 27 | 28 | end 29 | 30 | 31 | def on_error(exception) 32 | end -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ### tests 19 | ``` 20 | fastlane tests 21 | ``` 22 | 23 | ### beta 24 | ``` 25 | fastlane beta 26 | ``` 27 | 28 | ### release 29 | ``` 30 | fastlane release 31 | ``` 32 | 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 37 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 38 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 39 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/fastlane/actions/create_changelog.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | module SharedValues 4 | CREATE_CHANGELOG_CUSTOM_VALUE = :CREATE_CHANGELOG_CUSTOM_VALUE 5 | end 6 | 7 | class CreateChangelogAction < Action 8 | def self.run(params) 9 | 10 | UI.user_error!("You need to have the git-chglog cli installed! Aborted \u{1F6D1}") unless system("which git-chglog") 11 | 12 | # fastlane will take care of reading in the parameter and fetching the environment variable: 13 | UI.message "Parameter Tag Pattern: #{params[:tag_pattern]}" 14 | UI.message "Parameter Next Tag Value: #{params[:next_tag]}" 15 | 16 | sh "git-chglog -o CHANGELOG.md --tag-filter-pattern #{params[:tag_pattern]} --next-tag #{params[:next_tag]}" 17 | 18 | # Actions.lane_context[SharedValues::CREATE_CHANGELOG_CUSTOM_VALUE] = "my_val" 19 | end 20 | 21 | ##################################################### 22 | # @!group Documentation 23 | ##################################################### 24 | 25 | def self.description 26 | "Generate a changelog from your commit messages and tags" 27 | end 28 | 29 | def self.details 30 | # Optional: 31 | # this is your chance to provide a more detailed description of this action 32 | "This action will use `git-chglog` to create an automatic changelog from your commit messages. \n 33 | You can specify the tag pattern that should be used for grouping the commits. \n 34 | This will only work if you are conforming to the `Conventional Commit Specification`." 35 | end 36 | 37 | def self.available_options 38 | # Define all options your action supports. 39 | 40 | # Below a few examples 41 | [ 42 | FastlaneCore::ConfigItem.new(key: :tag_pattern, 43 | env_name: "FL_CREATE_CHANGELOG_TAG_PATTERN", # The name of the environment variable 44 | description: "Tag pattern to be used for grouping", # a short description of this parameter 45 | is_string: true, # true: verifies the input is a string, false: every kind of value 46 | verify_block: proc do |value| 47 | UI.user_error!("No tag pattern for CreateChangelogAction given, pass using `tag_pattern: 'pattern'`") unless (value and not value.empty?) 48 | # UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value) 49 | end), 50 | FastlaneCore::ConfigItem.new(key: :next_tag, 51 | env_name: "FL_CREATE_CHANGELOG_NEXT_TAG", 52 | description: "Next Tag is used to create a new release in the changelog without having a tag in git", 53 | is_string: true, # true: verifies the input is a string, false: every kind of value 54 | verify_block: proc do |value| 55 | UI.user_error!("No next tag for CreateChangelogAction given, pass using `next_tag: 'newtag'`") unless (value and not value.empty?) 56 | # UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value) 57 | end) 58 | ] 59 | end 60 | 61 | def self.output 62 | # Define the shared values you are going to provide 63 | # Example 64 | [ 65 | ['CREATE_CHANGELOG_CUSTOM_VALUE', 'A description of what this value contains'] 66 | ] 67 | end 68 | 69 | def self.return_value 70 | # If your method provides a return value, you can describe here what it does 71 | end 72 | 73 | def self.authors 74 | ["alexanderwe"] 75 | end 76 | 77 | def self.is_supported?(platform) 78 | # you can do things like 79 | # 80 | # true 81 | # 82 | # platform == :ios 83 | # 84 | # [:ios, :mac].include?(platform) 85 | # 86 | 87 | platform == :ios 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /{{cookiecutter.projectDirectory}}/fastlane/actions/crowdin.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | module SharedValues 4 | CROWDIN_CUSTOM_VALUE = :CROWDIN_CUSTOM_VALUE 5 | end 6 | 7 | class CrowdinAction < Action 8 | def self.run(params) 9 | UI.user_error!("crowdin.yml is required to run this lane! Aborted \u{1F6D1}") unless File.exist?("#{params[:config_file]}") 10 | UI.user_error!("You need to have the crowdin cli installed! Aborted \u{1F6D1}") unless system("which crowdin") 11 | 12 | # fastlane will take care of reading in the parameter and fetching the environment variable: 13 | UI.message "Parameter crowdin CLI Action: #{params[:command]}" 14 | UI.message "Parameter crowdin CLI config file: #{params[:config_file]}" 15 | 16 | sh "crowdin #{params[:command]} --config #{params[:config_file]}" 17 | 18 | # Actions.lane_context[SharedValues::CROWDIN_CUSTOM_VALUE] = "my_val" 19 | end 20 | 21 | ##################################################### 22 | # @!group Documentation 23 | ##################################################### 24 | 25 | def self.description 26 | "This action is a wrapper around the crowdin cli" 27 | end 28 | 29 | def self.details 30 | # Optional: 31 | # this is your chance to provide a more detailed description of this action 32 | "This action should serve as a convenience wrapper around the crowdin cli to be used in CI/CD environments" 33 | end 34 | 35 | def self.available_options 36 | # Define all options your action supports. 37 | 38 | [ 39 | FastlaneCore::ConfigItem.new(key: :command, 40 | env_name: "FL_CROWDIN_COMMAND", # The name of the environment variable 41 | description: "Command for the crowdin cli", # a short description of this parameter 42 | verify_block: proc do |value| 43 | UI.user_error!("No CLI command provided, pass using `command: 'command'`") unless (value and not value.empty?) 44 | # UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value) 45 | end), 46 | FastlaneCore::ConfigItem.new(key: :config_file, 47 | env_name: "FL_CROWDIN_CONFIG_FILE", 48 | description: "Path to the config file for crowdin", 49 | default_value: "crowdin.yml", 50 | verify_block: proc do |value| 51 | UI.user_error!("No crowdin config file provided, pass using `config_file: 'path'`") unless (value and not value.empty?) 52 | UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value) 53 | end) 54 | ] 55 | end 56 | 57 | def self.authors 58 | ["alexanderwe"] 59 | end 60 | 61 | def self.is_supported?(platform) 62 | # you can do things like 63 | # 64 | # true 65 | # 66 | # platform == :ios 67 | # 68 | # [:ios, :mac].include?(platform) 69 | # 70 | 71 | platform == :ios 72 | end 73 | end 74 | end 75 | end 76 | --------------------------------------------------------------------------------