swift-ios
Skill2 starsChanged 37 days agoMITView on GitHub
Opinionated rules for production native iOS apps in Swift and SwiftUI, covering the Observation framework, Swift concurrency (async/await, actors, Sendable), URLSession networking against a JSON API, Keychain credential storage, StoreKit 2 purchases with server-side entitlements, testing, and App Store / TestFlight release workflows. Use when building or reviewing SwiftUI views, view models, networking, authentication, in-app purchases, concurrency code, or preparing an App Store submission.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- its-ash/skillstheir own version, first seen Nov 1, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- its-ash/skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository5 other files it gives its agents.
- expo-react-nativeSkill
- fullstack-architectSkill
- nextjs-frontendSkill
- postgresqlSkill
- python-fastapiSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 1 is distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Swift concurrency only: Use async/await, structured tasks, and actors for new code.
- Very specific: line 30
Credentials live only in the Keychain: Never in UserDefaults, files, or @AppStorage.
- Very specific: line 111
Async work in views: use .task { }, or .task(id: query) to restart when an input changes.
- Very specific: line 115
Sign in: use Sign in with Apple (SignInWithAppleButton) or Google via ASWebAuthenticationSession/SwiftUI's webAuthenticationSession, with …
- Very specific: line 139
Unexpected view updates: call let _ = Self._printChanges() in body (debug builds only).
- Very specific: line 147
Use List or LazyVStack with stable identifiers for long content.
- Very specific: line 149
Keep body cheap: precompute display strings in the model, and use FormatStyle instead of creating formatters in body.
- Very specific: line 157
Use os.Logger: interpolated values are private by default.
- Very specific: line 158
Write sensitive files with .completeFileProtection.
- Distinctive: line 175
Release: use phased release for automatic updates.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Swift / SwiftUI iOS
Build native iOS apps with SwiftUI, Observation, and Swift concurrency. The app consumes the same API as the web and Expo apps. Targets Swift 6 language mode and iOS 17+ (Observation). If the deployment target is lower, adapt the Observation rules to ObservableObject.
Choose native Swift over Expo when the product needs deep platform integration (widgets and Live Activities as core features, watchOS, heavy media or AR, advanced background modes) or an existing native codebase. The decision belongs in the architecture doc (see fullstack-architect).
When to use
- Writing SwiftUI views, navigation, or
@Observablemodels. - Networking with URLSession, auth flows, or token storage.
- Concurrency work: async/await, tasks, actors, Sendable diagnostics.
- In-app purchases and subscriptions with StoreKit 2.
- Tests, TestFlight, App Store submission, review rejections.
Core rules
- Views are functions of state. Logic, loading, and side effects live in
@MainActor @Observablemodels and services, never inbodyor a view'sinit. Views get recreated constantly. - Swift concurrency only. Use async/await, structured tasks, and actors for new code. Wrap legacy callback APIs once with
withCheckedThrowingContinuation. Don't useDispatchQueuein new code except for interop. - Swift 6 strict concurrency. Fix Sendable and isolation diagnostics properly.
@unchecked Sendableandnonisolated(unsafe)need a comment that proves safety, or they don't merge. - UI state is MainActor-isolated. Shared mutable non-UI state (API client, token store, caches) lives in actors.
- Credentials live only in the Keychain. Never in
UserDefaults, files, or@AppStorage. No secrets compiled into the binary: anything in the IPA can be extracted. - The server is the source of truth for authorization and entitlements. The client caches them for UX.
- Every screen handles loading, empty, error-with-retry, and success. Cancellation is not an error: don't show alerts for
CancellationErrororURLError.cancelled.
Architecture
App/
├── AcmeApp.swift # @main, composition root: builds services, injects via .environment
├── Core/ # move to a local Swift package as the app grows (faster builds, enforced boundaries)
│ ├── Networking/ # APIClient (actor), Endpoint, APIError, JSON coding
│ ├── Auth/ # TokenStore (actor, Keychain), SessionModel (@Observable)
│ ├── Purchases/ # StoreKit 2 wrapper, entitlement sync with the API
│ └── Persistence/ # SwiftData/GRDB cache, if offline is needed (a cache, not the source of truth)
├── Features/
│ └── Events/
│ ├── EventsView.swift
│ ├── EventsModel.swift # @MainActor @Observable
│ └── EventRow.swift
├── DesignSystem/ # reusable components, typography, colors
└── Resources/ # Assets, Localizable.xcstrings, PrivacyInfo.xcprivacy- Dependency injection: build services in the composition root and pass them through initializers or
.environment(_:). No global singletons (APIClient.shared) reached from deep inside features. They make tests and previews hard. - Navigation:
NavigationStack(path:)with value-basednavigationDestination(for:), and a router model that owns the path. Deep links and notification taps append to the path.
Preferred patterns
@MainActor @Observable
final class EventsModel {
enum State { case loading, empty, loaded([Event]), failed(String) }
private(set) var state: State = .loading
private var nextCursor: String?
private let api: APIClient
init(api: APIClient) { self.api = api }
func load() async {
state = .loading
do {
let page = try await api.send(.upcomingEvents(cursor: nil))
nextCursor = page.nextCursor
state = page.items.isEmpty ? .empty : .loaded(page.items)
} catch is CancellationError {
// the view went away; keep the current state
} catch {
state = .failed(error.userMessage)
}
}
}
struct EventsView: View {
@State private var model: EventsModel
init(api: APIClient) { _model = State(initialValue: EventsModel(api: api)) }
var body: some View {
content
.task { await model.load() } // starts on appear, cancels on disappear
.refreshable { await model.load() }
}
@ViewBuilder private var content: some View {
switch model.state {
case .loading: ProgressView()
case .empty: ContentUnavailableView("No upcoming events", systemImage: "calendar")
case .loaded(let events): List(events) { EventRow(event: $0) }
case .failed(let message):
ContentUnavailableView {
Label("Couldn't load events", systemImage: "wifi.exclamationmark")
} description: { Text(message) } actions: {
Button("Try again") { Task { await model.load() } }
}
}
}
}- Observation: own models with
@State, pass them down as plain properties, and use@Bindablewhen a child needs bindings. Use@Environmentfor app-wide services. Don't useObservableObject/@Published/@StateObjectin new iOS 17+ code. - Async work in views: use
.task { }, or.task(id: query)to restart when an input changes. Don't useonAppear { Task { ... } }, which never cancels. - Networking: one
APIClientactor with a configuredURLSession(timeouts,waitsForConnectivitywhere appropriate) and oneJSONDecoder. It attaches the bearer token, maps Problem Details errors to a typedAPIErrorusing the API'scode, and on 401 runs a single-flight token refresh, then retries once. - Dates: the API returns ISO 8601 UTC with fractional seconds (Pydantic emits microseconds).
JSONDecoder.DateDecodingStrategy.iso8601rejects fractional seconds, so use a custom strategy and test it with a fixture captured from the real API. - Keychain: generic password items with
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly(useWhenUnlockedThisDeviceOnlyif nothing refreshes in the background). Keychain items survive app deletion, so wipe them on the first launch after a fresh install. - Sign in: use Sign in with Apple (
SignInWithAppleButton) or Google viaASWebAuthenticationSession/SwiftUI'swebAuthenticationSession, with PKCE. Send the identity token to the API, which verifies it and issues its own tokens.
Networking and Keychain implementations are in references/networking-and-keychain.md. StoreKit 2 with server-side entitlements is in references/storekit.md.
Patterns to avoid
| Avoid | Do instead |
Network calls or heavy work in body or init | .task calling an @Observable model |
@ObservedObject for an object the view creates | @State with an @Observable model |
onAppear { Task { ... } } | .task { } (cancels automatically) |
DispatchQueue.main.async to silence threading warnings | Correct @MainActor isolation |
Task.detached by default | Structured tasks. Detach only with a stated reason |
try!, as!, force unwraps on server data | Typed decoding with explicit errors |
Tokens in UserDefaults or @AppStorage | Keychain |
APIClient.shared used everywhere | Injected dependencies |
.iso8601 decoding strategy for API dates | A custom strategy that accepts fractional seconds |
AnyView in lists and hot paths | @ViewBuilder and concrete types |
| Unlocking premium content based only on a client flag | Server-verified entitlements |
verifyReceipt (deprecated) | StoreKit 2 + App Store Server API / Server Notifications V2 |
Debugging
- Concurrency: enable Thread Sanitizer and the Main Thread Checker in the scheme, and read Swift 6 diagnostics as design feedback. A data race warning means shared mutable state without an owner.
- Unexpected view updates: call
let _ = Self._printChanges()inbody(debug builds only). Use the SwiftUI instrument to find expensive or frequent body evaluations. - Decoding failures: print
DecodingErrorcases in full (key path + debug description). The usual causes are date formats, nulls where non-optional, and unknown enum values (decode unknown cases to an.unknownfallback). - Network: log the API's
X-Request-IDresponse header with every failure, so backend logs can be correlated. Inspect traffic with Proxyman or Charles in debug builds. - Memory: Instruments Leaks/Allocations. Closures stored on models need
[weak self]. A closure passed to anasynccall usually doesn't. - Crashes: upload dSYMs from CI (Xcode Cloud and fastlane can do this as part of the build) and read symbolicated reports in Xcode Organizer or your crash reporter.
Performance
- Use
ListorLazyVStackwith stable identifiers for long content. Paginate with the API cursor. - Request thumbnail sizes from the server.
AsyncImagehas no tunable cache, so use a caching loader for image-heavy lists. - Keep
bodycheap: precompute display strings in the model, and useFormatStyleinstead of creating formatters inbody. - Observation tracks properties per access. Small subviews that read only what they show re-render less.
- Measure launch with the App Launch instrument and defer non-critical setup until after first frame.
Security
- Keychain with
ThisDeviceOnlyaccessibility. Clear the Keychain,URLCache, and local stores on sign-out. - App Transport Security stays on. No arbitrary loads.
- Use
os.Logger: interpolated values are private by default. Mark only non-sensitive valuesprivacy: .public. - Write sensitive files with
.completeFileProtection. - Validate universal link and notification payloads before navigating or acting.
- Jailbreak detection is not a control. Server-side authorization is.
Testing
- Swift Testing (
@Test,#expect,#require, parameterized tests) for models, decoding, and the API client. Use XCTest for UI tests (XCUITest) and performance tests. - API client: stub responses with a
URLProtocolsubclass. Test request building, decoding of real captured fixtures, Problem Details error mapping, and 401 → single refresh → retry → sign-out. - Models: inject a fake API (protocol or closure-based) and assert state transitions: loading → loaded, empty, failed, and cancellation ignored.
- StoreKit: a StoreKit configuration file plus
SKTestSessionfor purchase, renewal, refund, and Ask to Buy flows. - Manual matrix: the oldest supported iOS, the smallest device, the largest Dynamic Type size, VoiceOver on core flows, dark mode, and offline.
App Store workflow
- Versions:
CFBundleShortVersionStringis the marketing version (bump it per release).CFBundleVersionis the build number (monotonic, set by CI). - CI: Xcode Cloud, or fastlane on your CI with managed signing (
match), produces signed builds and uploads them to TestFlight. - TestFlight: internal testers first. External groups need beta app review.
- Release: use phased release for automatic updates. Pause it if crash rates rise.
- Privacy:
PrivacyInfo.xcprivacydeclares required-reason APIs (e.g.UserDefaults, file timestamps) and any tracking domains. Third-party SDKs must ship their own manifests. App Privacy labels in App Store Connect must match real data collection. - Common rejection causes: no in-app account deletion when accounts can be created; third-party login without an equivalent privacy-focused option such as Sign in with Apple; digital goods sold outside in-app purchase where the storefront rules don't allow it (these rules differ by region and have changed recently, so check current guideline 3.1); user-generated content without reporting, blocking, and filtering; vague permission purpose strings; no demo account for review.
- Apple raises the minimum Xcode/SDK for uploads periodically. Keep CI on a current Xcode.
Production-readiness checklist
- Swift 6 language mode. No unexplained
@unchecked Sendableornonisolated(unsafe). - Tokens are in the Keychain (
ThisDeviceOnly), wiped on sign-out and on first launch after reinstall. - Every screen handles loading, empty, error-with-retry, and offline. Cancellations are silent.
- The API client decodes captured fixtures, including dates with fractional seconds and unknown enum values.
- 401 → single refresh → one retry → sign-out, covered by tests.
- StoreKit:
Transaction.updateslistener started at launch, transactions finished after granting, entitlements verified server-side, a Restore Purchases button exists. - Privacy manifest, App Privacy labels, and purpose strings are accurate. Account deletion exists.
- dSYMs are uploaded and crash reporting is verified with a test crash.
- Accessibility pass (Dynamic Type, VoiceOver labels). Tested on the oldest supported iOS.
- CI-managed build numbers, a TestFlight pass before every release, phased release enabled.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

