| name | ios |
| description | - Building iOS, iPadOS, or macOS apps with SwiftUI |
Swift + SwiftUI iOS Development
When to activate
- Building iOS, iPadOS, or macOS apps with SwiftUI
- Implementing async/await networking with URLSession
- Setting up Core Data for local persistence
- Wiring Combine publishers and subscribers
- Preparing for App Store submission and review
- Debugging property wrapper selection (
@State, @Binding, @ObservedObject, etc.)
When NOT to use
- UIKit-only projects where SwiftUI is explicitly excluded
- tvOS or watchOS targets with significantly different lifecycle
- Server-side Swift (Vapor, Hummingbird) without any UI layer
Instructions
Property Wrappers — When Each
| Wrapper | Owner | Use case |
|---|
@State | The view itself | Local, value-type UI state (toggle, text field input, counter) |
@Binding | Parent passes it down | Two-way connection to a parent's @State |
@StateObject | The view itself | View creates and owns a reference-type ObservableObject |
@ObservedObject | Parent passes it in | View observes a reference-type object owned elsewhere |
@EnvironmentObject | Injected via .environmentObject() | Shared observable object propagated through the view hierarchy |
@Environment | SwiftUI environment | System values: colorScheme, dismiss, openURL, custom environment keys |
struct ToggleRow: View {
@State private var isOn = false
var body: some View {
Toggle("Notifications", isOn: $isOn)
}
}
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Notifications", isOn: $isOn)
}
}
struct ProfileView: View {
@StateObject private var viewModel = ProfileViewModel()
var body: some View {
Text(viewModel.name)
}
}
struct ProfileView: View {
@ObservedObject var viewModel: ProfileViewModel
body: {
(viewModel.name)
}
}
: {
session:
body: {
(session.displayName)
}
}
().environmentObject(())
Rule: use @StateObject when the view creates the object. Use @ObservedObject when the object is injected. Never create an ObservableObject directly in the body property — it will be recreated on every render.
Async/Await with URLSession
struct User: Decodable {
let id: Int
let name: String
let email: String
}
struct APIClient {
let baseURL = URL(string: "https://api.example.com")!
let decoder = JSONDecoder()
func fetchUser(id: Int) async throws -> User {
let url = baseURL.appendingPathComponent("users/\(id)")
var request = URLRequest(url: url)
request.setValue("Bearer \(TokenStore.current)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard ().contains(http.statusCode) {
.httpError(http.statusCode)
}
decoder.decode(., from: data)
}
}
: {
user: ?
error: ?
isLoading
client ()
(: ) {
isLoading
{ isLoading }
{
user client.fetchUser(id: id)
} {
.error error
}
}
}
: {
vm ()
userId:
body: {
{
vm.isLoading {
()
} user vm.user {
(user.name)
} error vm.error {
()
}
}
.task { vm.load(id: userId) }
}
}
Always mark ViewModels @MainActor to ensure @Published mutations happen on the main thread.
Core Data — NSPersistentContainer Setup
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let controller = PersistenceController(inMemory: true)
let ctx = controller.container.viewContext
let item = Item(context: ctx)
item.timestamp = Date()
try? ctx.save()
return controller
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data failed: ") }
}
container.viewContext.automaticallyMergesChangesFromParent
container.viewContext.mergePolicy
}
}
: {
persistence .shared
body: {
{
()
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
}
: {
(\.managedObjectContext) ctx
(
sortDescriptors: [(\.timestamp, order: .reverse)],
animation: .default
)
items: <>
() {
item (context: ctx)
item.timestamp ()
ctx.save()
}
}
For background operations, use container.newBackgroundContext() or container.performBackgroundTask { ctx in }.
Combine — sink and assign
import Combine
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results: [Product] = []
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
private let api = APIClient()
init() {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.handleEvents(receiveOutput: { [weak self] _ in self?.isLoading = true })
.flatMap { [weak self] q -> AnyPublisher<[Product], Never> in
guard let self else { return Empty().eraseToAnyPublisher() }
.api.search(query: q)
.catch { ([]) }
.eraseToAnyPublisher()
}
.receive(on: .main)
.handleEvents(receiveOutput: { [ ] .isLoading })
.assign(to: )
}
}
Use .sink when you need side effects. Use .assign(to:) to drive a @Published property. Always store subscriptions in Set<AnyCancellable> or use the &$published form to avoid premature cancellation.
App Store Submission Checklist
Before submitting to App Store Connect:
Technical requirements:
- Build with the latest stable Xcode and latest iOS SDK
- Minimum deployment target set correctly in project settings
- No use of private APIs (App Store review scans for these)
- NSAppTransportSecurity exceptions documented and justified
- All required permission strings in
Info.plist (camera, location, microphone, contacts, etc.)
- Icons for all required sizes present in asset catalog (Xcode generates from a 1024x1024 source)
- Launch screen or
LaunchScreen.storyboard configured
Privacy:
- Privacy manifest (
PrivacyInfo.xcprivacy) required for apps using specific APIs (file timestamp APIs, system boot time, disk space, active keyboard, user defaults)
- Third-party SDKs must also include privacy manifests
- Declare data collection in App Store Connect (Data Types used, linked to user, tracking)
App Store Connect:
- Version and build number incremented from last submission
- Screenshots for all required device sizes (6.9", 6.5", 5.5" iPhone; 13" and 12.9" iPad if iPad supported)
- App Preview video optional but improves conversion
- Keywords field: comma-separated, 100 character limit
- Age rating questionnaire completed
- Export Compliance: declare if using encryption (HTTPS qualifies; standard exemption usually applies)
Testing before submission:
- TestFlight beta tested on physical device, not only simulator
- Deep links and Universal Links tested
- Push notifications tested end-to-end
- In-app purchase sandbox flow verified
- Accessibility audit (VoiceOver, Dynamic Type)
Example
A settings screen demonstrating multiple property wrappers together:
@MainActor
class SettingsViewModel: ObservableObject {
@Published var notificationsEnabled = false
@Published var theme: AppTheme = .system
func save() async {
try? await UserPreferencesAPI.save(
notifications: notificationsEnabled,
theme: theme
)
}
}
struct SettingsView: View {
@StateObject private var vm = SettingsViewModel()
@EnvironmentObject var session: UserSession
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
Section("Account") {
Text(session.email).foregroundStyle(.secondary)
}
Section("Preferences") {
Toggle("Notifications", isOn: $vm.notificationsEnabled)
Picker("Theme", selection: $vm.theme) {
ForEach(AppTheme.allCases) { (.label).tag() }
}
}
}
.navigationTitle()
.toolbar {
(placement: .confirmationAction) {
() {
{
vm.save()
dismiss()
}
}
}
}
}
}