diff --git a/.github/actions/code-signing-setup/action.yml b/.github/actions/code-signing-setup/action.yml new file mode 100644 index 0000000..c5cfd1f --- /dev/null +++ b/.github/actions/code-signing-setup/action.yml @@ -0,0 +1,39 @@ +name: Set up Apple code signing +description: Installs code signing certificate(s) to a temporary keychain +inputs: + build-certificate-base64: + required: true + description: The base64-encoded p12 build certificate + p12-password: + required: true + description: The password for the p12 build certificate + keychain-password: + required: true + description: The password for the temporary keychain +runs: + using: "composite" + steps: + - name: Install code signing certificate + env: + BUILD_CERTIFICATE_BASE64: ${{ inputs.build-certificate-base64 }} + P12_PASSWORD: ${{ inputs.p12-password }} + KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }} + shell: bash + # Source: + # https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development + run: | + # Create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # Import certificate from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + + # Create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..59c9d13 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,119 @@ +name: Run tests and deploy to TestFlight + +on: + workflow_dispatch: + # Uncomment this to run the workflow on every push to the main branch: + # push: + # branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + XCODE_PROJECT: TemplateApp.xcodeproj + SCHEME: TemplateApp + TEST_PLAN: AllTests + TEST_RESULT_BUNDLE: TestResults.xcresult + ARCHIVE_PATH: TemplateApp.xcarchive + EXPORT_OPTIONS_PLIST: ExportOptions.plist + +jobs: + build: + name: Run tests and deploy to TestFlight + runs-on: [macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select Xcode version + run: sudo xcode-select --switch /Applications/Xcode_15.3.app + + - name: Cache Swift Package Manager dependencies + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/SourcePackages + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Install Swift packages + run: | + xcodebuild -project "${{ env.XCODE_PROJECT }}" \ + -scheme "${{ env.SCHEME }}" \ + -onlyUsePackageVersionsFromResolvedFile \ + -resolvePackageDependencies \ + -clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" + + - name: Install code signing certificate + uses: "./.github/actions/code-signing-setup" + with: + build-certificate-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + p12-password: ${{ secrets.P12_PASSWORD }} + keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} + + - name: Install App Store Connect API key + run: | + echo -n "${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}" | base64 --decode -o "${{ runner.temp }}/AuthKey.p8" + + - name: Run tests + env: + PLATFORM: ${{ 'iOS Simulator' }} + run: | + # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) + DEVICE=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` + xcodebuild test -project "${{ env.XCODE_PROJECT }}" \ + -scheme "${{ env.SCHEME }}" \ + -testPlan "${{ env.TEST_PLAN }}" \ + -destination "platform=$PLATFORM,name=$DEVICE" \ + -resultBundlePath "${{ env.TEST_RESULT_BUNDLE }}" \ + -clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" \ + -disableAutomaticPackageResolution + + - name: Upload test result bundle + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: ${{ env.SCHEME }}-${{ github.ref_name }}-${{ github.sha }}.xcresult + path: ${{ env.TEST_RESULT_BUNDLE }} + + - name: Archive build + run: | + xcodebuild clean archive -project "${{ env.XCODE_PROJECT }}" \ + -scheme "${{ env.SCHEME }}" \ + -destination generic/platform=iOS \ + -archivePath "${{ runner.temp }}/${{ env.ARCHIVE_PATH }}" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "${{ runner.temp }}/AuthKey.p8" \ + -authenticationKeyID ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \ + -authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} \ + -clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" \ + -disableAutomaticPackageResolution + + - name: Upload to TestFlight + run: | + xcodebuild -exportArchive \ + -allowProvisioningUpdates \ + -authenticationKeyPath "${{ runner.temp }}/AuthKey.p8" \ + -authenticationKeyID ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} \ + -authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} \ + -archivePath "${{ runner.temp }}/${{ env.ARCHIVE_PATH }}" \ + -exportPath ${{ env.ARCHIVE_PATH }} \ + -exportOptionsPlist ${{ env.EXPORT_OPTIONS_PLIST }} + + # To add Crashlytics dSYM upload, grap the `upload-symbols` script from the Firebase SDK from here: + # https://github.com/firebase/firebase-ios-sdk/blob/main/Crashlytics/upload-symbols + # and add it to the repository in a folder called `scripts`. Then uncomment the following lines: + + # - name: Upload dSYM files to Firebase Crashlytics + # run: | + # ./scripts/upload-symbols -p ios \ + # -gsp "TemplateApp/GoogleService-Info.plist" \ + # "${{ runner.temp }}/${{ env.ARCHIVE_PATH }}/dSYMs" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.SCHEME }}-${{ github.ref_name }}-${{ github.sha }}.xcarchive + path: ${{ runner.temp }}/${{ env.ARCHIVE_PATH }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fce0e58 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Run tests + +on: + pull_request: + branches: ["main"] + +env: + XCODE_PROJECT: TemplateApp.xcodeproj + SCHEME: TemplateApp + TEST_PLAN: AllTests + TEST_RESULT_BUNDLE: TestResults.xcresult + +jobs: + build: + name: Run tests + runs-on: [macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select Xcode version + run: sudo xcode-select --switch /Applications/Xcode_15.3.app + + - name: Cache Swift Package Manager dependencies + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/SourcePackages + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Install Swift packages + run: | + xcodebuild -project "${{ env.XCODE_PROJECT }}" \ + -scheme "${{ env.SCHEME }}" \ + -onlyUsePackageVersionsFromResolvedFile \ + -resolvePackageDependencies \ + -clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" + + - name: Run tests + env: + PLATFORM: ${{ 'iOS Simulator' }} + run: | + # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) + DEVICE=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` + xcodebuild test -project "${{ env.XCODE_PROJECT }}" \ + -scheme "${{ env.SCHEME }}" \ + -testPlan "${{ env.TEST_PLAN }}" \ + -destination "platform=$PLATFORM,name=$DEVICE" \ + -resultBundlePath "${{ env.TEST_RESULT_BUNDLE }}" \ + -clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" \ + -disableAutomaticPackageResolution + + - name: Upload test result bundle + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: ${{ env.SCHEME }}-${{ github.ref_name }}-${{ github.sha }}.xcresult + path: ${{ env.TEST_RESULT_BUNDLE }} diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..5ffbfff --- /dev/null +++ b/.swiftformat @@ -0,0 +1,3 @@ +--disable unusedArguments +--disable redundantRawValues +--indent 4 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..7ca66e6 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,4 @@ +disabled_rules: + - trailing_comma + - line_length + - unused_closure_parameter diff --git a/ExportOptions.plist b/ExportOptions.plist new file mode 100644 index 0000000..4238904 --- /dev/null +++ b/ExportOptions.plist @@ -0,0 +1,22 @@ + + + + + destination + upload + manageAppVersionAndBuildNumber + + method + app-store + signingStyle + automatic + stripSwiftSymbols + + teamID + XXXXXXXXXX + testFlightInternalTestingOnly + + uploadSymbols + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7b7758 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Q42 iOS template + +This is a template for creating iOS projects at Q42. It has opinionated defaults and boilerplate, based on how we do iOS projects at Q42. + +## How to use it + +1. In GitHub, press "use this template" to create a new repository. +2. Rename your project using the included Python script. + +## Features + +Only basic features that almost all projects use, were added in this template: + +* SwiftUI using the SwiftUI lifecycle with an AppDelegate +* Dependency injection using Factory +* Unit tests and UI tests using Salad +* GitHub Actions CI configuration that runs the tests and submits the app to TestFlight + +Xcode 15.3 or higher is required. + +## Code style + +The Xcode project is configured to use 4 spaces for indentation. +For linting Swift source code, we use [SwiftLint](https://github.com/realm/SwiftLint). +A configuration for [SwiftFormat](http://github.com/nicklockwood/SwiftFormat) is also included. + +## Continuous integration + +GitHub Actions is used for continuous integration (CI). The CI runs the automated tests when you make a pull request. +On a push to the `main` branch, it will also run the tests, and if they pass, a build of the app is made and uploaded to TestFlight. + +### CI configuration + +Five environment secrets are needed for the workflow to run on GitHub Actions. +You may configure these in the repository secret settings on GitHub. + +* `BUILD_CERTIFICATE_BASE64` contains a base64-encoded string of the .p12 certificate bundle, used to code sign the app. This bundle needs to contain two certificates: **development** and **distribution**. +* `P12_PASSWORD` contains the password of the certificate bundle. +* `APP_STORE_CONNECT_API_KEY_BASE64` contains a base64-encoded string of the .p8 App Store Connect API key. +* `APP_STORE_CONNECT_API_KEY_ID` contains the key ID of the App Store Connect API key. +* `APP_STORE_CONNECT_API_KEY_ISSUER_ID` contains the issuer ID of the App Store Connect API key. + +To create such a certificate bundle, open Keychain Access. Unfold the entries for the development and distribution certificate. Select the certificates and their private keys using shift, then right-click and select "Export 4 items...". + +You can encode a file to base64 on the command line like this: `base64 -i ~/Desktop/Certificates.p12 | pbcopy`. This automatically puts the result on your clipboard. diff --git a/TemplateApp.xcodeproj/project.pbxproj b/TemplateApp.xcodeproj/project.pbxproj index e3bed72..8edbb94 100644 --- a/TemplateApp.xcodeproj/project.pbxproj +++ b/TemplateApp.xcodeproj/project.pbxproj @@ -3,16 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ D5284F302B57C6B600BB32E7 /* TemplateAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F2F2B57C6B600BB32E7 /* TemplateAppApp.swift */; }; - D5284F322B57C6B600BB32E7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F312B57C6B600BB32E7 /* ContentView.swift */; }; + D5284F322B57C6B600BB32E7 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F312B57C6B600BB32E7 /* RootView.swift */; }; D5284F342B57C6B700BB32E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5284F332B57C6B700BB32E7 /* Assets.xcassets */; }; D5284F372B57C6B700BB32E7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5284F362B57C6B700BB32E7 /* Preview Assets.xcassets */; }; - D5284F412B57C6B700BB32E7 /* TemplateAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F402B57C6B700BB32E7 /* TemplateAppTests.swift */; }; - D5284F4B2B57C6B700BB32E7 /* TemplateAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F4A2B57C6B700BB32E7 /* TemplateAppUITests.swift */; }; + D5284F412B57C6B700BB32E7 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F402B57C6B700BB32E7 /* ExampleTests.swift */; }; + D5284F4B2B57C6B700BB32E7 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5284F4A2B57C6B700BB32E7 /* ExampleUITests.swift */; }; + D5F745E82BEE14870064F06A /* TemplateAppAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F745E72BEE14870064F06A /* TemplateAppAppDelegate.swift */; }; + D5F745EA2BEE14DD0064F06A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = D5F745E92BEE14DD0064F06A /* Localizable.xcstrings */; }; + D5F745EF2BEE15C20064F06A /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5F745EE2BEE15C20064F06A /* Launch Screen.storyboard */; }; + D5F745F52BEE48BC0064F06A /* Salad in Frameworks */ = {isa = PBXBuildFile; productRef = D5F745F42BEE48BC0064F06A /* Salad */; }; + D5F745F82BEE48F50064F06A /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F745F72BEE48F50064F06A /* RootView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,13 +40,21 @@ /* Begin PBXFileReference section */ D5284F2C2B57C6B600BB32E7 /* TemplateApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TemplateApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; D5284F2F2B57C6B600BB32E7 /* TemplateAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppApp.swift; sourceTree = ""; }; - D5284F312B57C6B600BB32E7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + D5284F312B57C6B600BB32E7 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; D5284F332B57C6B700BB32E7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D5284F362B57C6B700BB32E7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; D5284F3C2B57C6B700BB32E7 /* TemplateAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TemplateAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - D5284F402B57C6B700BB32E7 /* TemplateAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppTests.swift; sourceTree = ""; }; + D5284F402B57C6B700BB32E7 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; D5284F462B57C6B700BB32E7 /* TemplateAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TemplateAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - D5284F4A2B57C6B700BB32E7 /* TemplateAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppUITests.swift; sourceTree = ""; }; + D5284F4A2B57C6B700BB32E7 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; + D5F745E72BEE14870064F06A /* TemplateAppAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateAppAppDelegate.swift; sourceTree = ""; }; + D5F745E92BEE14DD0064F06A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + D5F745EB2BEE15210064F06A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + D5F745EE2BEE15C20064F06A /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + D5F745F02BEE15D70064F06A /* AllTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AllTests.xctestplan; sourceTree = ""; }; + D5F745F12BEE16E60064F06A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + D5F745F22BEE16FB0064F06A /* TemplateApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TemplateApp.entitlements; sourceTree = ""; }; + D5F745F72BEE48F50064F06A /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,6 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D5F745F52BEE48BC0064F06A /* Salad in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,6 +86,7 @@ D5284F232B57C6B600BB32E7 = { isa = PBXGroup; children = ( + D5F745EB2BEE15210064F06A /* README.md */, D5284F2E2B57C6B600BB32E7 /* TemplateApp */, D5284F3F2B57C6B700BB32E7 /* TemplateAppTests */, D5284F492B57C6B700BB32E7 /* TemplateAppUITests */, @@ -95,8 +110,11 @@ isa = PBXGroup; children = ( D5284F2F2B57C6B600BB32E7 /* TemplateAppApp.swift */, - D5284F312B57C6B600BB32E7 /* ContentView.swift */, + D5F745E72BEE14870064F06A /* TemplateAppAppDelegate.swift */, + D5284F312B57C6B600BB32E7 /* RootView.swift */, + D5F745E92BEE14DD0064F06A /* Localizable.xcstrings */, D5284F332B57C6B700BB32E7 /* Assets.xcassets */, + D5F745ED2BEE15B80064F06A /* Supporting Files */, D5284F352B57C6B700BB32E7 /* Preview Content */, ); path = TemplateApp; @@ -113,7 +131,7 @@ D5284F3F2B57C6B700BB32E7 /* TemplateAppTests */ = { isa = PBXGroup; children = ( - D5284F402B57C6B700BB32E7 /* TemplateAppTests.swift */, + D5284F402B57C6B700BB32E7 /* ExampleTests.swift */, ); path = TemplateAppTests; sourceTree = ""; @@ -121,11 +139,31 @@ D5284F492B57C6B700BB32E7 /* TemplateAppUITests */ = { isa = PBXGroup; children = ( - D5284F4A2B57C6B700BB32E7 /* TemplateAppUITests.swift */, + D5284F4A2B57C6B700BB32E7 /* ExampleUITests.swift */, + D5F745F62BEE48EF0064F06A /* ViewObjects */, ); path = TemplateAppUITests; sourceTree = ""; }; + D5F745ED2BEE15B80064F06A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D5F745F02BEE15D70064F06A /* AllTests.xctestplan */, + D5F745F12BEE16E60064F06A /* Info.plist */, + D5F745EE2BEE15C20064F06A /* Launch Screen.storyboard */, + D5F745F22BEE16FB0064F06A /* TemplateApp.entitlements */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D5F745F62BEE48EF0064F06A /* ViewObjects */ = { + isa = PBXGroup; + children = ( + D5F745F72BEE48F50064F06A /* RootView.swift */, + ); + path = ViewObjects; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -178,6 +216,9 @@ D5284F482B57C6B700BB32E7 /* PBXTargetDependency */, ); name = TemplateAppUITests; + packageProductDependencies = ( + D5F745F42BEE48BC0064F06A /* Salad */, + ); productName = TemplateAppUITests; productReference = D5284F462B57C6B700BB32E7 /* TemplateAppUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -207,14 +248,18 @@ }; }; buildConfigurationList = D5284F272B57C6B600BB32E7 /* Build configuration list for PBXProject "TemplateApp" */; - compatibilityVersion = "Xcode 14.0"; + compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, + nl, ); mainGroup = D5284F232B57C6B600BB32E7; + packageReferences = ( + D5F745F32BEE48BC0064F06A /* XCRemoteSwiftPackageReference "Salad" */, + ); productRefGroup = D5284F2D2B57C6B600BB32E7 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -233,6 +278,8 @@ files = ( D5284F372B57C6B700BB32E7 /* Preview Assets.xcassets in Resources */, D5284F342B57C6B700BB32E7 /* Assets.xcassets in Resources */, + D5F745EF2BEE15C20064F06A /* Launch Screen.storyboard in Resources */, + D5F745EA2BEE14DD0064F06A /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -257,7 +304,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D5284F322B57C6B600BB32E7 /* ContentView.swift in Sources */, + D5F745E82BEE14870064F06A /* TemplateAppAppDelegate.swift in Sources */, + D5284F322B57C6B600BB32E7 /* RootView.swift in Sources */, D5284F302B57C6B600BB32E7 /* TemplateAppApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -266,7 +314,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D5284F412B57C6B700BB32E7 /* TemplateAppTests.swift in Sources */, + D5284F412B57C6B700BB32E7 /* ExampleTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -274,7 +322,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D5284F4B2B57C6B700BB32E7 /* TemplateAppUITests.swift in Sources */, + D5F745F82BEE48F50064F06A /* RootView.swift in Sources */, + D5284F4B2B57C6B700BB32E7 /* ExampleUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -346,7 +395,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -403,7 +452,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -418,12 +467,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TemplateApp/TemplateApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"TemplateApp/Preview Content\""; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TemplateApp/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -447,12 +498,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TemplateApp/TemplateApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"TemplateApp/Preview Content\""; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TemplateApp/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -477,12 +530,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + CURRENT_PROJECT_VERSION = 1.2.3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateAppTests; + PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateApp.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -497,12 +549,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + CURRENT_PROJECT_VERSION = 1.2.3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.2; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateAppTests; + PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateApp.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -516,11 +567,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + CURRENT_PROJECT_VERSION = 1.2.3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateAppUITests; + PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateApp.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -534,11 +585,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 8J6VSUTQNX; + CURRENT_PROJECT_VERSION = 1.2.3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateAppUITests; + PRODUCT_BUNDLE_IDENTIFIER = com.q42.TemplateApp.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -587,6 +638,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D5F745F32BEE48BC0064F06A /* XCRemoteSwiftPackageReference "Salad" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Q42/Salad.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D5F745F42BEE48BC0064F06A /* Salad */ = { + isa = XCSwiftPackageProductDependency; + package = D5F745F32BEE48BC0064F06A /* XCRemoteSwiftPackageReference "Salad" */; + productName = Salad; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = D5284F242B57C6B600BB32E7 /* Project object */; } diff --git a/TemplateApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TemplateApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..e3436c6 --- /dev/null +++ b/TemplateApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "929d2090e52375231c5ebc33be1479d4266be85bbf192a8f578aa5ddb5af112c", + "pins" : [ + { + "identity" : "salad", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Q42/Salad.git", + "state" : { + "revision" : "c10580e0632ddaf1542334d386ee3118e1074204", + "version" : "1.0.0" + } + } + ], + "version" : 3 +} diff --git a/TemplateApp.xcodeproj/xcshareddata/xcschemes/TemplateApp.xcscheme b/TemplateApp.xcodeproj/xcshareddata/xcschemes/TemplateApp.xcscheme new file mode 100644 index 0000000..d894ae9 --- /dev/null +++ b/TemplateApp.xcodeproj/xcshareddata/xcschemes/TemplateApp.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TemplateApp/AllTests.xctestplan b/TemplateApp/AllTests.xctestplan new file mode 100644 index 0000000..d641a47 --- /dev/null +++ b/TemplateApp/AllTests.xctestplan @@ -0,0 +1,41 @@ +{ + "configurations" : [ + { + "id" : "2E268531-76BE-4986-BE2E-F7971FD484AB", + "name" : "English", + "options" : { + "language" : "en", + "region" : "GB" + } + }, + { + "enabled" : false, + "id" : "AEB26D7A-9FE8-4E6A-B095-01F3DA26EFCA", + "name" : "Dutch", + "options" : { + "language" : "nl", + "region" : "NL" + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:TemplateApp.xcodeproj", + "identifier" : "D5284F3B2B57C6B700BB32E7", + "name" : "TemplateAppTests" + } + }, + { + "target" : { + "containerPath" : "container:TemplateApp.xcodeproj", + "identifier" : "D5284F452B57C6B700BB32E7", + "name" : "TemplateAppUITests" + } + } + ], + "version" : 1 +} diff --git a/TemplateApp/Assets.xcassets/AccentColor.colorset/Contents.json b/TemplateApp/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..9d08b4a 100644 --- a/TemplateApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/TemplateApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "45", + "green" : "188", + "red" : "132" + } + }, "idiom" : "universal" } ], diff --git a/TemplateApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/TemplateApp/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..11f81c8 100644 --- a/TemplateApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/TemplateApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "q42-icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/TemplateApp/Assets.xcassets/AppIcon.appiconset/q42-icon.png b/TemplateApp/Assets.xcassets/AppIcon.appiconset/q42-icon.png new file mode 100644 index 0000000..b95fce9 Binary files /dev/null and b/TemplateApp/Assets.xcassets/AppIcon.appiconset/q42-icon.png differ diff --git a/TemplateApp/Info.plist b/TemplateApp/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/TemplateApp/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/TemplateApp/Launch Screen.storyboard b/TemplateApp/Launch Screen.storyboard new file mode 100644 index 0000000..dccc0e7 --- /dev/null +++ b/TemplateApp/Launch Screen.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TemplateApp/Localizable.xcstrings b/TemplateApp/Localizable.xcstrings new file mode 100644 index 0000000..e023044 --- /dev/null +++ b/TemplateApp/Localizable.xcstrings @@ -0,0 +1,16 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Hello, world!" : { + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hallo, wereld!" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/TemplateApp/ContentView.swift b/TemplateApp/RootView.swift similarity index 58% rename from TemplateApp/ContentView.swift rename to TemplateApp/RootView.swift index 9fc9857..e05e76c 100644 --- a/TemplateApp/ContentView.swift +++ b/TemplateApp/RootView.swift @@ -1,13 +1,13 @@ // -// ContentView.swift +// RootView.swift // TemplateApp // -// Created by Mathijs Bernson on 17/01/2024. +// Copyright © 2024 Q42. All rights reserved. // import SwiftUI -struct ContentView: View { +struct RootView: View { var body: some View { VStack { Image(systemName: "globe") @@ -16,9 +16,11 @@ struct ContentView: View { Text("Hello, world!") } .padding() + .accessibilityElement(children: .contain) + .accessibilityIdentifier("RootView") } } #Preview { - ContentView() + RootView() } diff --git a/TemplateApp/TemplateApp.entitlements b/TemplateApp/TemplateApp.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/TemplateApp/TemplateApp.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/TemplateApp/TemplateAppApp.swift b/TemplateApp/TemplateAppApp.swift index 79e68dc..faeac5f 100644 --- a/TemplateApp/TemplateAppApp.swift +++ b/TemplateApp/TemplateAppApp.swift @@ -2,16 +2,18 @@ // TemplateAppApp.swift // TemplateApp // -// Created by Mathijs Bernson on 17/01/2024. +// Copyright © 2024 Q42. All rights reserved. // import SwiftUI @main struct TemplateAppApp: App { + @UIApplicationDelegateAdaptor var appDelegate: TemplateAppAppDelegate + var body: some Scene { WindowGroup { - ContentView() + RootView() } } } diff --git a/TemplateApp/TemplateAppAppDelegate.swift b/TemplateApp/TemplateAppAppDelegate.swift new file mode 100644 index 0000000..67d4d6b --- /dev/null +++ b/TemplateApp/TemplateAppAppDelegate.swift @@ -0,0 +1,15 @@ +// +// TemplateAppAppDelegate.swift +// TemplateApp +// +// Copyright © 2024 Q42. All rights reserved. +// + +import UIKit + +class TemplateAppAppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + // Additional app setup can be performed here. + return true + } +} diff --git a/TemplateAppTests/TemplateAppTests.swift b/TemplateAppTests/ExampleTests.swift similarity index 92% rename from TemplateAppTests/TemplateAppTests.swift rename to TemplateAppTests/ExampleTests.swift index 2d734be..c14976d 100644 --- a/TemplateAppTests/TemplateAppTests.swift +++ b/TemplateAppTests/ExampleTests.swift @@ -1,15 +1,14 @@ // -// TemplateAppTests.swift +// ExampleTests.swift // TemplateAppTests // -// Created by Mathijs Bernson on 17/01/2024. +// Copyright © 2024 Q42. All rights reserved. // import XCTest @testable import TemplateApp final class TemplateAppTests: XCTestCase { - override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } @@ -26,5 +25,4 @@ final class TemplateAppTests: XCTestCase { // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. XCTAssertEqual(21 * 2, 42) } - } diff --git a/TemplateAppUITests/ExampleUITests.swift b/TemplateAppUITests/ExampleUITests.swift new file mode 100644 index 0000000..09c1540 --- /dev/null +++ b/TemplateAppUITests/ExampleUITests.swift @@ -0,0 +1,36 @@ +// +// TemplateAppUITests.swift +// TemplateAppUITests +// +// Created by Mathijs Bernson on 17/01/2024. +// + +// +// ExampleUITests.swift +// TemplateAppTests +// +// Copyright © 2024 Q42. All rights reserved. +// + +import XCTest +import Salad + +final class ExampleUITests: XCTestCase { + var scenario: Scenario! + + override func setUp() { + continueAfterFailure = false + + let app = XCUIApplication() + app.launch() + scenario = Scenario(given: app) + } + + func testExample() { + scenario + .then { rootView in + XCTAssertTrue(rootView.identifyingElement.staticTexts["Hello, world!"].waitForExist(timeout: .asyncUI), + "Expected to see 'Hello, world!' label") + } + } +} diff --git a/TemplateAppUITests/TemplateAppUITests.swift b/TemplateAppUITests/TemplateAppUITests.swift deleted file mode 100644 index 412d550..0000000 --- a/TemplateAppUITests/TemplateAppUITests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TemplateAppUITests.swift -// TemplateAppUITests -// -// Created by Mathijs Bernson on 17/01/2024. -// - -import XCTest - -final class TemplateAppUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // 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. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - -} diff --git a/TemplateAppUITests/ViewObjects/RootView.swift b/TemplateAppUITests/ViewObjects/RootView.swift new file mode 100644 index 0000000..8f775d0 --- /dev/null +++ b/TemplateAppUITests/ViewObjects/RootView.swift @@ -0,0 +1,15 @@ +// +// RootView.swift +// TemplateAppUITests +// +// Created by Mathijs Bernson on 10/05/2024. +// Copyright © 2024 Q42. All rights reserved. +// + +import XCTest +import Salad + +struct RootView: ViewObject { + let root: XCUIElement + let identifyingElementId: String = "RootView" +} diff --git a/scripts/rename-project.py b/scripts/rename-project.py new file mode 100644 index 0000000..011fd89 --- /dev/null +++ b/scripts/rename-project.py @@ -0,0 +1,64 @@ +""" +This script renames the template project to the desired name. +""" + +import os +from pathlib import Path + +folder = Path(os.path.abspath(os.path.dirname(__file__))).parent.as_posix() + +dryRun = False +oldProjectName = "TemplateApp" +print("Enter new project name:") +newProjectName = input() + +# ========= Rename folders: + +print( + "\nRenaming '%s' to '%s' in folder names.\n" % (oldProjectName, newProjectName) +) + +for root, dirs, files in os.walk(folder, topdown=False): + for subDir in dirs: + if oldProjectName in subDir: + oldFolderName = os.path.join(root, subDir) + newFolderName = os.path.join(root, subDir.replace(oldProjectName, newProjectName)) + if dryRun: + print("Would rename folder: %s to %s" % (oldFolderName, newFolderName)) + else: + print("Renaming folder: %s to %s" % (oldFolderName, newFolderName)) + os.rename(oldFolderName, newFolderName) + +# ========= Rename usages in source files: ========= + +print( + "\nReplacing all occurrences of %s in source files with: '%s'.\n" % (oldProjectName, newProjectName) +) + +def replace_package_name_occurences_in_file(filename): + print("Would update file: " + filename) + with open(filename, "r") as file: + filedata = file.read() + + if oldProjectName in filedata: + filedata = filedata.replace(oldProjectName, newProjectName) + + if dryRun: + print("Would update file: " + filename) + else: + print("Updating file: " + filename) + with open(filename, "w") as file: + file.write(filedata) + file.close() + +for root, dirs, files in os.walk(folder, topdown=False): + allowed_extensions = ["swift", "plist", "yml", "pbxproj", "storyboard", "xctestplan", "xcscheme"] + for name in files: + extension = name.split(".")[-1] + if extension in allowed_extensions: + file_name = os.path.join(root, name) + replace_package_name_occurences_in_file(file_name) + +print( + "\nDone renaming project to: '%s'.\n" % (newProjectName) +)