diff --git a/.vscode/launch.json b/.vscode/launch.json index 1784ff83..0c75b6e1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,8 +6,9 @@ "args": [], "cwd": "${workspaceFolder:brightdigit.com}", "name": "Debug brightdigitwg", - "program": "${workspaceFolder:brightdigit.com}/.build/debug/brightdigitwg", - "preLaunchTask": "swift: Build Debug brightdigitwg" + "preLaunchTask": "swift: Build Debug brightdigitwg", + "target": "brightdigitwg", + "configuration": "debug" }, { "type": "swift", @@ -15,8 +16,9 @@ "args": [], "cwd": "${workspaceFolder:brightdigit.com}", "name": "Release brightdigitwg", - "program": "${workspaceFolder:brightdigit.com}/.build/release/brightdigitwg", - "preLaunchTask": "swift: Build Release brightdigitwg" + "preLaunchTask": "swift: Build Release brightdigitwg", + "target": "brightdigitwg", + "configuration": "release" } ] } \ No newline at end of file diff --git a/Content/tutorials/mise-setup-guide.md b/Content/tutorials/mise-setup-guide.md index 4c8e8974..fa42ace0 100644 --- a/Content/tutorials/mise-setup-guide.md +++ b/Content/tutorials/mise-setup-guide.md @@ -1,8 +1,8 @@ --- title: Getting Started with Mise for iOS and Swift Development date: 2026-04-22 12:00 -description: A guide to setting up Mise tool version management for Xcode projects - and Swift Packages, with real-world examples and migration patterns. +description: A guide to setting up Mise tool version management for iOS and Swift + projects, covering local development and GitHub Actions CI integration. tags: mise, tooling, swift, xcode, devops, ci-cd featuredImage: /media/tutorials/mise-setup-guide/mise-setup-guide-hero.webp subscriptionCTA: Want to stay up-to-date with the latest Swift tooling and CI/CD @@ -16,13 +16,14 @@ Over the years, I've used a variety of tools from Homebrew to Mint. Each have ha Mise is a tool version manager. There are a lot out there but Mise fit what I was looking for. Here's what I have tried and didn't quite fit: -### HomeBrew + +### Homebrew -HomeBrew is great for installing tools and apps locally on my computer. I recently purchased a brand new 15-inch M5 MacBook Air and rather than restoring from a backup I used Brew Bundle to restore the list of apps from my old MacBook Air easily. However for development tools, it's not really ideal especially in the case of Continuous Integration (automated build servers like GitHub Actions). **I want an isolated environment when I use CI** in order to have a repeatable environment regardless of OS or where the machine is hosted. So that fell short. Lastly handling multiple versions of language tools (ruby, node, etc...) is not ideal and is better suited with specific tools (rbenv, nvm, etc...). +Homebrew is great for installing tools and apps locally on my computer. I recently purchased a brand new 15-inch M5 MacBook Air and rather than restoring from a backup I used Brew Bundle to restore the list of apps from my old MacBook Air easily. However for development tools, it's not really ideal especially in the case of Continuous Integration (automated build servers like GitHub Actions). **I want an isolated environment when I use CI** in order to have a repeatable environment regardless of OS or where the machine is hosted. So that fell short. Lastly handling multiple versions of language tools (ruby, node, etc...) is not ideal and is better suited with specific tools (rbenv, nvm, etc...). ### Swift Package Plugin -There are several reasons I don't use Swift Package plugins but the biggest is the lack of support distinguishing between a consumer of a swift package vs a developer of a Swift package. I really don't want a consumer to need to pull Swift tools I use (swift-format, swiftlint, xcodegen, periphery, stringslint, etc...) as a develop when trying to consume my library. There are proposals and [tools](https://github.com/shibapm/Rocket) that try to remediate this but it really hasn't stuck. Therefore it's best to configure these outside of Package.swift. +There are several reasons I don't use Swift Package plugins but the biggest is the lack of support distinguishing between a consumer of a swift package vs a developer of a Swift package. I really don't want a consumer to need to pull Swift tools I use (swift-format, swiftlint, xcodegen, periphery, stringslint, etc...) as a developer when trying to consume my library. There are proposals and [tools](https://github.com/shibapm/Rocket) that try to remediate this but it really hasn't stuck. Therefore it's best to configure these outside of Package.swift. ### Mint @@ -32,6 +33,7 @@ Mint is a fantastic tool for installing Swift Package based tools. As a Swift de 2. Only suited for Swift Packages. If you have a web application attached (node) or require fastlane for deployment (ruby), you'll need an additional tool to do this. 3. Every swift package is rebuilt; so deployment via binaries is unavailable meaning long build times just to get the tool up and running. + ### Why Mise Mise solves a lot of these issues: @@ -50,6 +52,7 @@ For my projects, it manages everything from Node.js to Swift tooling. The first thing you are going to want to do is install mise. + ### 1. Install Mise ```bash @@ -64,6 +67,8 @@ echo 'eval "$(mise activate zsh)"' >> ~/.zshrc # zsh echo 'eval "$(mise activate bash)"' >> ~/.bashrc # bash ``` +Once shell integration is active, tools managed by mise are available directly — just run `swiftlint lint`, `tuist generate`, etc. and mise automatically uses the pinned version. The `mise exec --` prefix is still useful in scripts or CI environments where shell integration isn't set up. + ### 2. Create `.mise.toml` at Repository Root `.mise.toml` is the configuration file that declares which tools and versions your project needs. It lives at the repository root and gets checked into version control alongside your code. Once it's there, any developer — or CI runner — who clones the repo gets the exact same tool versions by running `mise install`. No more "it works on my machine" surprises. @@ -75,18 +80,16 @@ experimental = true [tools] # Swift tools via SPM -"spm:swiftlang/swift-format" = "601.0.0" -"spm:peripheryapp/periphery" = "3.1.0" -"spm:apple/swift-openapi-generator" = "1.7.0" +"spm:swiftlang/swift-format" = "602.0.0" # Linting (via core or aqua) -swiftlint = "0.58.0" +swiftlint = "0.63.2" ``` **Critical Settings Explained:** - `experimental = true` — Enables the SPM backend for Swift Package tools -- `spm:/` — Tells mise to install a tool by building it from a Swift Package. The format mirrors a GitHub slug: `spm:swiftlang/swift-format` maps to `github.com/swiftlang/swift-format`. +- `spm:/` — Tells mise to install a tool by building it from a Swift Package. ### 3. Create/Update `Makefile` @@ -98,8 +101,8 @@ install-dependencies: @mise install ``` -**How It Works:** -- `make install-dependencies` installs all tools from `.mise.toml` +**Why use Make here:** +Using a named Make target standardizes the setup command across team members, CI scripts, and onboarding docs. `make install-dependencies` is easier to document and remember than `mise install` alone — and it scales if you need to add other setup steps (e.g. npm install, pre-commit hooks) in the future. ### 4. Update GitHub Actions Workflow @@ -114,9 +117,6 @@ jobs: # This replaces multiple tool setup actions - uses: jdx/mise-action@v4 - with: - install: true - cache: true ``` ### 5. Test Locally @@ -129,32 +129,7 @@ mise install mise list ``` - -## Key Patterns from Production - -### ✅ Always Do This - -1. **Enable experimental**: `experimental = true` (for SPM backend) -2. **Pin exact versions**: `swiftlint = "0.58.0"` not `"0.58"` -3. **Use mise-action@v4 in CI**: Replaces 5+ setup actions - -### ❌ Common Pitfalls - -1. **Version drift**: Pin exact versions — `swiftlint = "0.58.0"` not `"0.58"` - -### Backend Selection Guide - -| Tool Type | Backend | Example | -| ------------------ | ------- | ------------------------------------------ | -| Swift tools | `spm` | `"spm:swiftlang/swift-format" = "601.0.0"` | -| Fast binaries | `aqua` | `"aqua:realm/SwiftLint" = "0.58.0"` | - -**When to use which backend:** -- **core**: First choice for popular tools (Node, Ruby) -- **spm**: Swift Package Manager tools (swift-format, periphery) -- **aqua**: Fast alternative for tools in Aqua registry - ---- +> **Note for new clones:** When a developer clones this repo for the first time, mise will not activate tools until they explicitly trust the config. Run `mise trust` once in the repo directory before `mise install`. ## Quick Reference Commands @@ -171,14 +146,8 @@ mise exec swiftlint -- swiftlint lint # Check mise setup mise doctor -# Clear cache if issues -rm -rf ~/.mise/cache && mise install - # Show tool versions in current directory mise current - -# Upgrade mise itself -brew upgrade mise ``` --- @@ -201,22 +170,6 @@ eval "$(mise activate zsh)" mise exec swiftlint -- swiftlint version ``` -### SPM Tools Failing to Install - -**Problem:** Swift Package tools fail to build - -**Solution:** -```bash -# Ensure experimental is enabled -grep "experimental = true" .mise.toml - -# Clear SPM cache -rm -rf ~/.mise/installs/spm - -# Reinstall -mise install -``` - ### Version Mismatch in CI **Problem:** CI uses different version than local @@ -231,16 +184,32 @@ git commit -m "Pin tool versions with mise" grep "jdx/mise-action@v4" .github/workflows/*.yml ``` +### Tools Not Activating After Clone + +**Problem:** mise shows an "untrusted" warning and tools are not available after cloning a repo + +**Solution:** +```bash +# Trust the repo's mise config +mise trust + +# Then install tools +mise install +``` + --- ## Conclusion -A few things worth keeping in mind as you get started: +Setting up mise takes about ten minutes, but the payoff is a development environment that works identically on every machine and in every CI run. Commit `.mise.toml`, run `mise trust` on a fresh clone, and `mise install` does the rest — no more setup docs, no more version drift. + +You can get an idea of my current toolset and where _mise_ fits in: -- **Commit `.mise.toml`** to version control — that's what makes tool versions consistent across your team and in CI. -- **Use the `spm:` prefix** for Swift Package tools like swift-format and periphery. -- **`mise install` is all you need** — any developer who clones your repo can be up and running with a single command. -- **One step in GitHub Actions** replaces all your individual tool-setup boilerplate. The `jdx/mise-action` reads the same `.mise.toml` your teammates use locally. +
+Mise development tools and setup overview +
+ +In the next article, we'll talk about a major piece of this setup - [Tuist and how I use it to simplify my Xcode project setup](/tutorials/tuist-xcode-project-setup/). --- @@ -250,10 +219,6 @@ A few things worth keeping in mind as you get started: - **GitHub Action**: [jdx/mise-action][3] - **Mise Registry**: [mise.jdx.dev/registry.html][5] ---- - -This guide is based on production implementations across my app projects. - [2]: https://mise.jdx.dev [3]: https://github.com/jdx/mise-action [5]: https://mise.jdx.dev/registry.html diff --git a/Content/tutorials/tuist-xcode-project-setup.md b/Content/tutorials/tuist-xcode-project-setup.md new file mode 100644 index 00000000..da158687 --- /dev/null +++ b/Content/tutorials/tuist-xcode-project-setup.md @@ -0,0 +1,414 @@ +--- +title: Automating your Xcode Project +date: 2026-04-27 9:00 +description: Before any CI/CD automation can run, you need a project structure worth + automating. This part covers Xcode project generation with Tuist, keeping all real + code in Swift Packages, and the package topologies that work for apps of every size. +tags: tuist, xcode, swift, ci-cd, tooling +featuredImage: /media/tutorials/tuist-xcode-project-setup/tuist-xcode-hero.webp +subscriptionCTA: Want to stay up-to-date with the latest Swift tooling and CI/CD + tips? Sign up for the newsletter to get notified when new tutorials drop. +--- + +When creating an app, let’s think about what we need to get started. The Xcode project is the backbone of your application — it contains metadata like bundle identifiers and permission text, along with build configuration for your targets, which can include the app itself, frameworks, and app extensions. + +The Xcode project uses a _proprietary format_ that is notoriously difficult to work with, especially when dealing with merge conflicts in version control. + +This is where a tool which creates the Xcode project is most helpful. There’s 2 leading tools which I’d recommend: Xcodegen or Tuist. Xcodegen is great if you have a fairly simple app or minimal team structure. Xcodegen uses YAML for its specification structure — if you prefer a simpler static config format, it’s the right choice. For a broader look at both tools, see [How to automate iOS development](/articles/ios-automation/). + +Tuist is what I’d recommend in most any other case. Tuist uses Swift for its manifest files, which is a deliberate design choice — it gives you type-checking and the full power of the language for complex configurations. Tuist has a very robust community and support as well. In the end I’d highly recommend **not** committing Xcode projects to your code repository. + +# Using Tuist + +If you've followed the [mise setup guide](/tutorials/mise-setup-guide/), add Tuist to your `.mise.toml` and you're set: + +```toml +[tools] +tuist = "4.188.1" +``` + +Pinning to a specific version ensures everyone on the team — and your CI environment — uses the exact same Tuist build. `latest` can silently pull in a breaking release and break your project generation unexpectedly. + +Homebrew also works for a quick local install: + +```bash +brew install tuist +``` + +[Homebrew](/tutorials/mise-setup-guide/#homebrew) works for local use, but doesn't give you per-project version pinning or a consistent CI environment. See [Why Mise](/tutorials/mise-setup-guide/#why-mise) in the setup guide for the full comparison. + +With [mise shell integration](/tutorials/mise-setup-guide/#shell-integration) active, run Tuist commands directly — mise automatically uses the pinned version. Without shell integration, prefix commands with `mise exec tuist --`. + +You can begin using Tuist by running: + +``` +tuist init +``` + +For my app Lumemo, a memo-taking iPhone app for iOS 26, here is the `Project.swift` Tuist generated: + +```swift +import ProjectDescription + +let project = Project( + name: "Lumemo", + targets: [ + .target( + name: "Lumemo", + destinations: .iOS, + product: .app, + bundleId: "dev.tuist.Lumemo", + infoPlist: .extendingDefault( + with: [ + "UILaunchScreen": [ + "UIColorName": "", + "UIImageName": "", + ], + ] + ), + buildableFolders: [ + "Lumemo/Sources", + "Lumemo/Resources", + ], + dependencies: [] + ), + .target( + name: "LumemoTests", + destinations: .iOS, + product: .unitTests, + bundleId: "dev.tuist.LumemoTests", + infoPlist: .default, + buildableFolders: [ + "Lumemo/Tests" + ], + dependencies: [.target(name: "Lumemo")] + ), + ] +) +``` + +This will get you started with the Tuist infrastructure. Tuist uses Swift for creating Xcode projects, so editing the project is fairly simple. You can edit `Project.swift` directly in any text editor, or if you want Xcode's type-checking and autocomplete while editing your manifest files, run: + +``` +tuist edit +``` + +This creates a temporary Xcode workspace that can compile your manifest Swift files — useful for catching typos or checking API signatures. When you're done, just close the workspace. Tuist writes your changes back to the manifest files automatically; no manual save is needed before closing. + +To create the Xcode project and workspace, just call: + +``` +tuist generate +``` + +This will create the Xcode project and workspace where you can work on your app. + +> **Don't edit the generated Xcode project directly.** Any changes you make to `.xcodeproj` settings inside Xcode are not stored in your manifest files — the next time you run `tuist generate`, they will be silently overwritten. Always make changes in `Project.swift` (or via `tuist edit`). + +Now that we've set up our first project using Tuist, let's dive into how the Xcode project works. The generated `Project.swift` uses `buildableFolders`, but for the walkthrough below we'll use the simpler `sources` parameter — it's easier to reason about as we build up to the final version. + +## Projects and their Targets + +An Xcode target is the core of our Xcode project. It produces the testable and deliverable app. Here's what a typical `Project.swift` looks like at this early stage: + +```swift +import ProjectDescription + +let project = Project( + name: "Lumemo", + targets: [ + .target( + name: "Lumemo", + destinations: [.iPhone], + product: .app, + bundleId: "com.brightdigit.Lumemo", + sources: ["Sources/**"] + ) + ] +) +``` + +Let’s take a look at how these map out: + + +
+Diagram showing how the Tuist Project.swift files lines up with Xcode +
Diagram showing how the Tuist Project.swift files lines up with Xcode.
+
+
+ +At the top, we have the name of the project "Lumemo". Inside we have out sets of targets. Typically Tuists sets up a test target but we removing that for simplicity sake for now. + +### Targets + +A target could be anything from a app extension to a framework to a unit tests. Esentially anything which could in that target spot. These different types (app extension, framework, etc...) are defined as _product types_. In tuist, we use an enum called `Product`. In our case this is an `.app`. + +We have a few identifiers here including the target name and the bundle ID. The bundle identifier must follow _reverse DNS naming_ and is unique to the App Store. The target name is the name shown in the target list. Unless you specifically supply a product name, the target name will be used as the product name (i.e. the name of the .ipa, .app, .pkg, etc.). + +In this case we are setting our destination to `.iPhone`. This can be a variety of destinations — not only the platform (iOS) or device (iPad), but also cases where you want to target macOS or visionOS from an iPad app, or build a Catalyst app for macOS. + +
+Xcode Supported Destinations dropdown showing iPhone, iPad, Mac, Apple TV, and Apple Vision options +
The full range of destinations available — from iPhone-only to multiplatform apps targeting Mac, Apple TV, and Apple Vision.
+
+ +Last but not least are your source files, which accept an array of glob strings. These are added to your project as source files and compiled accordingly. + +Now that we have the basics, we need to address a few gaps before this project is ready for App Store deployment: + +* **Notice the os version is set to the very latest based on the default of your Xcode version.** We should set this so it's stable across Xcode versions. +* **We are missing an app icon.** +* **We can't build it because we don't have a deployment team defined.** +* **Small App Store required info is missing** such as exempt encryption use and our privacy manifest +* **Lastly we don't have a stable way to define the app version.** + +Let's go through each of these. + +### Deployment Targets + +As stated earlier, destination defines not just the platform or device but what technology is used to deploy to a particular. This usually means the device class. However in cases where you are catalyst for macOS or allowing an iPad app's destination on a Mac or Vision Pro these specifics are required. + +To define the actual platform versions, you'd use `deploymentTargets`. If this isn't set, Xcode will use whatever the latest version available for that version of Xcode. + +Under the hood, `DeploymentTargets` is just a set of properties for each Apple platform. In the `Project` intializer you can set the individual platform using one the static methods below: + +```swift + /// Convenience method for `iOS` only minimum version + public static func iOS(_ version: String) -> DeploymentTargets + + /// Convenience method for `macOS` only minimum version + public static func macOS(_ version: String) -> DeploymentTargets + + /// Convenience method for `watchOS` only minimum version + public static func watchOS(_ version: String) -> DeploymentTargets + + /// Convenience method for `tvOS` only minimum version + public static func tvOS(_ version: String) -> DeploymentTargets + + /// Convenience method for `visionOS` only minimum version + public static func visionOS(_ version: String) -> DeploymentTargets +``` + +In our case we'll be targeting the minimum deployment version for iOS 26.0: + +```swift + deploymentTargets: .iOS("26.0"), +``` + +
+Xcode Minimum Deployments section showing iOS 26.0 +
With deploymentTargets set, Xcode shows the exact minimum iOS version you specified.
+
+ +Just as with `destinations` we can set variety of destinations, we have can create a multiplatform app as well: + +```swift + /// Multiplatform deployment target + public static func multiplatform(iOS: String? = nil, macOS: String? = nil, watchOS: String? = nil, tvOS: String? = nil, visionOS: String? = nil) -> ProjectDescription.DeploymentTargets +``` + + +### App Icon + +The app store requires App Icons before deployment. In our case since we are targeting a minimum of 26.0 we can use Icon Composer to create our app icon. To add it to app package, we just reference the file as a resource: + +```swift + resources: [ + "Resources/AppIcon.icon", + ... +``` + +Xcode should automatically pick that up and use it. + +Asset catalogs, text files, etc... any other resource would go here and a glob pattern can be used here as well. + +### Privacy Manifest + +The Privacy Manifest is required too and can go under the resources property as well. Apple's [Privacy manifest files documentation](https://developer.apple.com/documentation/bundleresources/privacy-manifest-files) is the authoritative reference for building one. + + +### Extending Other Default + +Besides the privacy manifest, there's the `ITSAppUsesNonExemptEncryption` setting. If you ever tried to submit an app's first version to app review, you've seen this question asked. + +
+App Store export compliance dialog showing ITSAppUsesNonExemptEncryption +
+ +Luckily we can skip this step by simply supplying this property in our Info.plist file. + +One thing great about Tuist, is that a lot of boiler plate and default values are already supplied to us for things like the Info.plist and the build settings. In our case we'll be using the `.extendingDefault(with:)` method to set `ITSAppUsesNonExemptEncryption` to `false`. + +
+Xcode Signing tab showing 'Signing for Lumemo requires a development team' error +
Without a development team set, Xcode refuses to build for a device.
+
+ +Let's do something like this for setting the development team. If you try to compile the app it will instantly complain that now development team is set. We can do this using the build settings and set it as a base property for all configurations: + +```swift + settings: .settings( + base: [ + "DEVELOPMENT_TEAM": "XXXXXXXXXX", // Your 10-character Apple Developer Team ID + ] + ) +``` + +Both of these functions for Info.plist and settings provide a plethora of property values that most users don't need to touch. This allows us to simply provide only the few overrides we need. + +The last gap to close is version management. + +### Version Management with xcconfig + +An app and its Xcode project contain 2 pieces of version information: + +* _Marketing Version_ (1.0.0) - this follows the typical semver string pattern and is what the user will see when they see the version number in the app store. +* _Build Number_ (39) - this is a unique integer that identifies each build uploading to the App Store which could be for submitting to app review or it could be submited to TestFlight or not even submitted at all. + +These settings are stored in the Info.plist. The _Marketing Version_ is stored as a string as the property `CFBundleShortVersionString`. The _Build Number_ is stored as `CFBundleVersion`. Of course we just hard code these in our Project.swift file for tuist to consume: + +```swift + infoPlist: .extendingDefault( + with: [ + "CFBundleShortVersionString": "1.0.0", + "CFBundleVersion": "1", + "ITSAppUsesNonExemptEncryption": false, + ] + ), +``` + +However if you want to automate incrementing or updating the build number each time, a more reliable approach than depending on a regular expression to update these values is an `xcconfig` file. `xcconfig` files are well-documented and simple to understand — they are similar to `.env` files but with additional capabilities. Let's create a new file `Config/Version.xcconfig` and set the version info there: + +``` +MARKETING_VERSION = 1.0.0 +CURRENT_PROJECT_VERSION = 2 +``` + +As you can tell it's just a name-value pair, and we are using Xcode's standard nomenclature for these value names. + +To let Tuist know about these we can import these into our build settings and configuration: + +```swift + settings: .settings( + base: [ + "DEVELOPMENT_TEAM": "XXXXXXXXXX", // Your 10-character Apple Developer Team ID + ], + configurations: [ + .debug(xcconfig: .relativeToRoot("Config/Version.xcconfig")), + .release(xcconfig: .relativeToRoot("Config/Version.xcconfig")), + ] + ) +``` + +Notice we are making sure we import into both the `debug` and `release` configurations. Also, since `xcconfig` is the native method for build configurations in Xcode, Tuist will reference the `xcconfig` file directly in the generated Xcode project. + +Lastly we need to reference these properties in our Info.plist. To refer to a specific property we use the variable notation of : + +``` +$(ALL_UPPER_CASE_SNAKE_CASE_PROPERTY_FROM_CONFIGURATION_SETTINGS) +``` + +So in our case, our `infoPlist` would look like: + +```swift + infoPlist: .extendingDefault( + with: [ + "CFBundleShortVersionString": "$(MARKETING_VERSION)", + "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)", + "ITSAppUsesNonExemptEncryption": false, + ] + ), +``` + +This will make it much easier to increment the build number or change the marketing version. + +### Ignoring Our Project + +This is what we should have for our end result: + +```swift +import ProjectDescription + +let project = Project( + name: "Lumemo", + targets: [ + .target( + name: "Lumemo", + destinations: [.iPhone], + product: .app, + bundleId: "com.brightdigit.Lumemo", + deploymentTargets: .iOS("26.0"), + infoPlist: .extendingDefault( + with: [ + "CFBundleShortVersionString": "$(MARKETING_VERSION)", + "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)", + "ITSAppUsesNonExemptEncryption": false, + ] + ), + sources: ["Sources/**"], + resources: [ + "Resources/AppIcon.icon", + "Resources/PrivacyInfo.xcprivacy", + ], + settings: .settings( + base: [ + "DEVELOPMENT_TEAM": "XXXXXXXXXX", // Your 10-character Apple Developer Team ID + ], + configurations: [ + .debug(xcconfig: .relativeToRoot("Config/Version.xcconfig")), + .release(xcconfig: .relativeToRoot("Config/Version.xcconfig")), + ] + ) + ) + ] +) +``` + +This will give us a fully working Xcode project but before we commit this to our repository, **we need to make sure we aren't committing our project and workspace.** + +If you don't already have a .gitignore file, [toptal has a great resource for creating one](https://www.toptal.com/developers/gitignore). I even have a url I download from everytime: + +``` +https://www.toptal.com/developers/gitignore/api/xcode,swift,swiftpackagemanager,swiftpm,macos +``` + +This should give me everything I need for my Xcode project except that I'll need to ignore the workspace and project file. Search for the line: + +``` +# *.xcodeproj +``` + +If you can't find it, you can just add the line instead of replacing the commented out one with this: + +``` +*.xcodeproj +``` + +We'll want to do the same thing for Xcode workspaces, so make sure we have the line: + +``` +*.xcworkspace +``` + +Lastly, derived files or caches from Tuist should be ignored as well: + +``` +.tuist/ +Derived/ +``` + +Now would be a good time to commit and push this to your repo. Every time someone pulls this repo, they should be able to use `mise` to execute `tuist` and generate the Xcode workspace and project. + +### One More Thing + +For a typical app, a few more target properties are worth knowing about. Depending on what your app does, you may need one or more of these before submitting to the App Store: + +* `entitlements` - these are permissions your app requires such as HealthKit, App Groups, etc... - this is set via a [Dictionary](https://developer.apple.com/documentation/bundleresources/entitlements) +* `Info.plist` - there are a variety of settings you may need for your application + * Apple Watch Companion Settings - `WKApplication`, `WKCompanionAppBundleIdentifier`, `WKRunsIndependentlyOfCompanionApp` + * File and URL Types - `CFBundleDocumentTypes`, `CFBundleURLTypes`, `UTExportedTypeDeclarations`, `UTImportedTypeDeclarations` + * Usage Descriptions - text for various access requests like geolocation, HealthKit, etc... + +### Where does all the source code go? + +Right now you should have a fully buildable app. However, this structure can be fairly limiting in terms of source code organization and compatibility. We'll be talking about how we can break down our application's source code into Swift Packages for easier testing, flexible OS compatibility, and easier modularization. diff --git a/Resources/media/tutorials/mise-setup-guide/mise-development-tools.webp b/Resources/media/tutorials/mise-setup-guide/mise-development-tools.webp new file mode 100644 index 00000000..79ae0383 Binary files /dev/null and b/Resources/media/tutorials/mise-setup-guide/mise-development-tools.webp differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/AppStore-ITSAppUsesNonExemptEncryption.png b/Resources/media/tutorials/tuist-xcode-project-setup/AppStore-ITSAppUsesNonExemptEncryption.png new file mode 100644 index 00000000..0f84da36 Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/AppStore-ITSAppUsesNonExemptEncryption.png differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/Swift-Automation-Tuist.webp b/Resources/media/tutorials/tuist-xcode-project-setup/Swift-Automation-Tuist.webp new file mode 100644 index 00000000..997050e7 Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/Swift-Automation-Tuist.webp differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/tuist-xcode-hero.webp b/Resources/media/tutorials/tuist-xcode-project-setup/tuist-xcode-hero.webp new file mode 100644 index 00000000..2db72cce Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/tuist-xcode-hero.webp differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/xcode-destinations-dropdown.webp b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-destinations-dropdown.webp new file mode 100644 index 00000000..2abbb1c3 Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-destinations-dropdown.webp differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/xcode-minimum-deployments.webp b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-minimum-deployments.webp new file mode 100644 index 00000000..97a92ea5 Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-minimum-deployments.webp differ diff --git a/Resources/media/tutorials/tuist-xcode-project-setup/xcode-signing-requires-team.webp b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-signing-requires-team.webp new file mode 100644 index 00000000..d2d561cb Binary files /dev/null and b/Resources/media/tutorials/tuist-xcode-project-setup/xcode-signing-requires-team.webp differ