Test iOS App Intents on a Cloud Mac: Parameters and Shortcuts

AI Automation ·~5 min read

Test iOS App Intents on a Cloud Mac: Parameters and Shortcuts

A “record reading” shortcut appears on a development machine but not in the simulator used for remote builds. Don’t change the app’s business logic yet. Valid parameters, a working Intent, and system discovery are three separate things to check. A cloud Mac provides a consistent environment for testing the first two; the last still needs to be verified in a simulator with the app installed.

Define What Automated Tests Can Cover

This walkthrough uses an example action with no disk writes: it accepts a book ID, trims leading and trailing whitespace, and returns the normalized ID. That keeps storage permissions and network variability out of the initial test. If you later connect a database, test writes and repeated execution separately. A successful return value does not mean the data was saved.

Verification has three layers: pure Swift tests check empty values and invalid characters; XCTest calls perform() to check return values and errors; and a person checks in the simulator that the action is visible, its parameter can be filled in, and its result can be read. Failures in the first two layers should block a merge. The third depends on system indexing, so treat it as a pre-release check rather than a test built around a fixed wait time.

Don’t repeatedly uninstall and reinstall the app in an automated test just to wait for the shortcut to appear. Prove the code contract first, then investigate installation, first launch, and system discovery.

Extract the Input Contract into a Testable Type

The following rule accepts 1–32 ASCII letters, digits, underscores, and hyphens. Whitespace is allowed only at the beginning or end of the input; other characters produce a specific error. In this example, perform() returns only the ID and has no persistence side effects.

import AppIntents
import Foundation

enum ReadingIDError: LocalizedError {
    case invalid

    var errorDescription: String? {
        "编号须为 1–32 位字母、数字、下划线或连字符"
    }
}

enum ReadingID {
    static func normalize(_ raw: String) throws -> String {
        let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        let pattern = #"^[A-Za-z0-9_-]{1,32}$"#
        guard value.range(of: pattern, options: .regularExpression) != nil else {
            throw ReadingIDError.invalid
        }
        return value
    }
}

struct CheckReadingIDIntent: AppIntent {
    static var title: LocalizedStringResource = "检查书籍编号"
    static var openAppWhenRun = false

    @Parameter(title: "书籍编号")
    var bookID: String

    func perform() async throws -> some IntentResult & ReturnsValue<String> {
        let normalized = try ReadingID.normalize(bookID)
        return .result(value: normalized)
    }
}

Put the parameter title, action title, and error message in the project’s localization resources. The Chinese strings here are example copy, not text to ship unchanged for every language in a multilingual app. The rule itself accepts only ASCII IDs. If your product allows Chinese characters or other scripts, define the permitted character set before changing the expression and test cases.

Test Edge Cases, Not Just Valid Input

At a minimum, verify that " book_7 " returns "book_7" and that an empty string, internal spaces, and IDs longer than 32 characters are rejected. Assert the error type on failure, not merely that “some error was thrown”; otherwise, an unexpected error from a dependency could make the test pass. When testing perform(), also confirm that its return value follows the normalization rule. Keep the pure rule tests separate from the Intent invocation tests so a failure tells you whether the input rule or the system API wrapper changed.

Register the Shortcut and Check It in the Simulator

To offer an action for the system to suggest, add an AppShortcutsProvider to the app target. Keep the application-name placeholder in the phrase so the system can associate the action with the current app; it is not a shell string.

import AppIntents

struct ReadingShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: CheckReadingIDIntent(),
            phrases: ["在 \(.applicationName) 检查书籍编号"],
            shortTitle: "检查书籍编号",
            systemImageName: "book"
        )
    }
}

First, confirm that this file belongs to the app target, not only the test target. Install and launch the built app once, then open Shortcuts in the simulator, search for the action title, enter a valid ID, and run it. Next, submit an invalid ID and check that the error is readable. If the action does not appear, record the OS version, app build version, and sequence of steps after installation. Don’t present “it happened to appear after reopening the screen” as a reliable fix.

Pin the Test Device and Result Directory

On a ZSHMac cloud Mac, run xcrun simctl list devices available to find an installed, available simulator. Choose its UDID and set SIM_UDID. The command below assumes the project has a shared scheme named Reader; use your own project’s scheme when running it. Give each run a new result-bundle path so an existing file does not cause the test command to fail immediately.

mkdir -p artifacts
test -n "${SIM_UDID:-}" || { printf '%s\n' 'SIM_UDID is required' >&2; exit 1; }
result="$PWD/artifacts/app-intents-$(date +%Y%m%d-%H%M%S)-$$.xcresult"
xcodebuild test \
  -scheme Reader \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath "$result"

In CI, retain both the command’s exit code and the result bundle. The exit code determines whether the job passes; the bundle helps identify failing test cases. If the scheme is not shared or the selected simulator is unavailable, fix the test setup first rather than classifying the failure as an Intent logic bug. For manual verification, record whether the action appears, whether the input prompt is correct, and what actually happens with valid and invalid parameters.

Troubleshoot One Layer at a Time

Symptom Check first Don’t start by
Pure rule test fails Character range, length, and trimming of leading and trailing whitespace Reinstalling the simulator
perform() returns an unexpected result Parameter passing and return-value type Waiting for system indexing
Test command cannot find the device Available device list and UDID Changing the Swift rule
Action is missing from Shortcuts App target membership, installation, and first launch Adding a fixed delay to the test
Action appears but fails when run Parameter prompt, error type, and target OS version Swallowing every error

The point of these layered checks is to narrow down the failure, not to guarantee that the system entry appears immediately after installation. Verification is complete only when the code tests pass and the simulator action has been checked with both valid and invalid input. Once you add real writes, add tests for repeated execution and data persistence as well; passing the “return the ID only” checks in this example will no longer be enough.

Frequently asked questions

Why is the action missing from Shortcuts when XCTest passes?

Confirm that the app containing AppShortcutsProvider is installed and has launched. Check the target configuration and OS version; system indexing can lag behind installation.

Where should App Intent parameter validation live?

Put reusable rules in a separate Swift type and call it from perform(). Unit tests can then check the rules without depending on system indexing.

Can headless CI replace a Shortcuts acceptance check?

No. CI can verify inputs and returned values; action visibility and parameter prompts still need checking on a simulator or device running the target OS.

Need your own cloud Mac?

Choose a model, location, and billing period. Each machine is a dedicated physical server. Availability is shown live in the console.

View plans and order