Category: 06. SwiftUI Data & Architecture

https://cdn3d.iconscout.com/3d/premium/thumb/ui-design-3d-icon-png-download-5846532.png

  • Testing SwiftUI

    Unit Test a ViewModel

    Place business logic in a ViewModel and verify it using XCTestCase.

    Syntax:

    • final class MyTests: XCTestCase { ... }
    • assertions like XCTAssertEqualXCTAssertTrue

    Example

    Demo.swift

    ContentView.swift

    App.swift

    CounterViewModelTests.swift

    import SwiftUI
    import Combine
    
    final class CounterViewModel: ObservableObject {
      @Published private(set) var count = 0
      func increment() { count += 1 }
    }
    
    struct CounterView: View {
      @StateObject private var vm = CounterViewModel()
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Count: \(vm.count)")
      Button("Increment") { vm.increment() }
    }
    } }

    This example verifies that calling increment() updates the published count.


    Prerequisites (UI Testing Target)

    • Add a UI Testing Bundle target (File → New → Target → UI Testing Bundle).
    • Prefer locating elements by accessibility identifiers rather than visible text.
    • Pass launch arguments and environment to control app state for tests (e.g., reset data, use fakes).

    UI Test a SwiftUI View

    Drive the app with XCUIApplication and locate elements by accessibility.

    Syntax:

    • let app = XCUIApplication(); app.launch()
    • app.buttons["Label"].tap()

    Example

    Demo.swift

    ContentView.swift

    App.swift

    MyUITests.swift

    import SwiftUI
    
    struct UITestDemo: View {
      @State private var count = 0
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Count: \(count)").accessibilityIdentifier("countLabel")
      Button("Increment") { count += 1 }
        .accessibilityIdentifier("incrementButton")
    }
    } }
  • SwiftUI AppStorage & SceneStorage

    SwiftUI AppStorage & SceneStorage

    Persist small values with @AppStorage and view-state per scene with @SceneStorage.


    @AppStorage (User Defaults)

    Bind a value directly to UserDefaults so changes persist across app launches.

    Syntax: @AppStorage("key") var value: T = default

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    
    struct AppStorageDemo: View {
      @AppStorage("username") private var username = ""
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Hello, \(username.isEmpty ? "Guest" : username)")
      TextField("Username", text: $username)
        .textFieldStyle(.roundedBorder)
    }
    .padding()
    } }

    This example stores the username in UserDefaults and keeps the UI in sync.


    @SceneStorage (Per-Scene UI State)

    Preserve transient UI data (like a draft) per window/scene; state is restored when the scene returns.

    Syntax: @SceneStorage("key") var value: T

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    
    struct SceneStorageDemo: View {
      @SceneStorage("draft_note") private var draft = ""
      var body: some View {
    
    VStack(alignment: .leading, spacing: 8) {
      Text("Draft:")
      TextEditor(text: $draft)
        .frame(minHeight: 120)
        .overlay(RoundedRectangle(cornerRadius: 6).stroke(.secondary))
    }
    .padding()
    } }
  • SwiftUI MVVM

    Model-View-ViewModel

    MVVM separates UI (View) from business logic (ViewModel) and data (Model).

    The ViewModel is an ObservableObject the View observes.

    Syntax:

    • class VM: ObservableObject { @Published var state }
    • @StateObject private var vm = VM()
    • List(vm.items) { ... }
    • ObservableObject: A class that notifies its observers when its state changes.
    • @Published: A property wrapper that publishes changes to a value, notifying observers.
    • @StateObject: A property wrapper that creates a new instance of a class, making it a source of truth for a view.

    Example

    Demo.swift

    ViewModel.swift

    ContentView.swift

    App.swift

    import SwiftUI
    
    struct MVVMBasicView: View {
      @StateObject private var vm = TodoViewModel()
      var body: some View {
    
    List(vm.todos) { t in Text(t.title) }
      .onAppear { vm.load() }
    } }

    This example shows the MVVM flow: a ViewModel exposes state, the View observes it, and triggers loading on appear.


    REMOVE ADS


    Sample App: Notes (MVVM)

    Example

    Demo.swift

    ViewModel.swift

    ContentView.swift

    App.swift

    import SwiftUI
    
    struct NotesMVVMView: View {
      @StateObject private var vm = NotesViewModel()
      var body: some View {
    
    List(vm.notes) { n in
      VStack(alignment: .leading) {
        Text(n.title).font(.headline)
        Text(n.body).font(.subheadline)
      }
    }
    .onAppear { vm.loadMock() }
    } }

    This example creates a simple Notes model and ViewModel, then renders a list and loads mock data when the view appears.

    Next, connect data to a server in Networking or persist locally in Core Data.


    App Group

    Use an App Group to share small values (like counts, flags, timestamps) between your app and its Widget.

    The hook below writes notesCount whenever the ViewModel’s notes change.

    How to use it:

    • Enable the same App Group on both the app target and the widget target (e.g., group.com.example.notes).
    • Keep updateSharedNotesCount writing to that group ID; the didSet observer writes the new count on every change.
    • Read the value from the widget using the same suiteName (see snippet below).

    App Group hook (Widget Count)

    Example

    NotesViewModel.swift

    import Combine
    
    func updateSharedNotesCount(_ count: Int) {
      let defaults = UserDefaults(suiteName: "group.com.example.notes")
      defaults?.set(count, forKey: "notesCount")
    }
    
    class NotesViewModel: ObservableObject {
      @Published var notes: [Note] = [] {
    
    didSet { updateSharedNotesCount(notes.count) }
    } func add(title: String, body: String) {
    notes.append(.init(id: UUID(), title: title, body: body))
    } func delete(at offsets: IndexSet) {
    notes.remove(atOffsets: offsets)
    } }

    Example

    WidgetRead.swift

    import SwiftUI
    
    // Option A: Direct read via UserDefaults (e.g., from a TimelineProvider)
    let defaults = UserDefaults(suiteName: "group.com.example.notes")
    let count = defaults?.integer(forKey: "notesCount") ?? 0
    
    // Option B: @AppStorage with a custom store (usable in a Widget view)
    struct WidgetCountView: View {
      @AppStorage("notesCount", store: UserDefaults(suiteName: "group.com.example.notes"))
      private var notesCount: Int = 0
    
      var body: some View {
    
    Text("Notes: \(notesCount)")
    } }
  • Persistence

    What is Core Data?

    Core Data is Apple’s object graph and persistence framework.

    It manages models, relationships, and change tracking, and can persist to SQLite under the hood.


    Basic Setup

    Add Core Data when creating the project, or create a NSPersistentContainer manually and pass its viewContext down to your SwiftUI views via .environment(\_.managedObjectContext).

    This example demonstrates a basic setup with a single entity and a list view.

    Syntax:

    • let container = NSPersistentContainer(name: "Model")
    • container.loadPersistentStores

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import CoreData
    
    @objc(Note)
    class Note: NSManagedObject {
      @NSManaged var title: String?
    }
    
    extension Note {
      @nonobjc class func fetchRequest() -> NSFetchRequest<Note> { NSFetchRequest<Note>(entityName: "Note") }
    }
    
    struct PersistenceController {
      static let shared = PersistenceController()
      let container: NSPersistentContainer
      init() {
    
    // Programmatic model so the sample runs without an .xcdatamodeld
    let model = NSManagedObjectModel()
    let entity = NSEntityDescription()
    entity.name = "Note"
    entity.managedObjectClassName = NSStringFromClass(Note.self)
    let title = NSAttributeDescription()
    title.name = "title"
    title.attributeType = .stringAttributeType
    title.isOptional = true
    entity.properties = &#91;title]
    model.entities = &#91;entity]
    container = NSPersistentContainer(name: "MyModel", managedObjectModel: model)
    let description = NSPersistentStoreDescription()
    description.type = NSInMemoryStoreType
    container.persistentStoreDescriptions = &#91;description]
    container.loadPersistentStores { _, error in
      if let error = error { fatalError("Unresolved error: \(error)") }
    }
    } } struct BasicCoreDataView: View { @Environment(\.managedObjectContext) private var context @FetchRequest(sortDescriptors: []) private var items: FetchedResults<Note> var body: some View {
    List(items, id: \.objectID) { note in Text(note.title ?? "") }
      .toolbar { Button("Add") { add() } }
    } private func add() {
    let i = Note(context: context)
    i.title = "New"
    try? context.save()
    } }

    This example initializes a persistent container and uses @Environment(\.managedObjectContext) with @FetchRequest to list and add items.

    The note about .xcdatamodeld is for running this in Xcode. It is not relevant for this example. But if you want to use Xcode, you can add a .xcdatamodeld file to your project.


    Sample App: Notes (Core Data)

    This example demonstrates a more complex setup with multiple entities and relationships.

    Syntax:

    • @Environment(\.managedObjectContext) for the context
    • @FetchRequest(sortDescriptors: [NSSortDescriptor(...)]) to query
    • context.save() to persist

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import CoreData
    
    class NoteEntity: NSManagedObject {
      @NSManaged public var id: UUID?
      @NSManaged public var title: String?
      @NSManaged public var body: String?
    }
    
    extension NoteEntity: Identifiable {}
    
    struct PersistenceController {
      static let shared = PersistenceController()
      let container: NSPersistentContainer
      init() {
    
    container = NSPersistentContainer(name: "MyModel")
    container.loadPersistentStores { _, error in
      if let error = error { fatalError("Unresolved error: \(error)") }
    }
    } } struct NotesCoreDataView: View { @Environment(\.managedObjectContext) private var context @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \NoteEntity.title, ascending: true)]) private var notes: FetchedResults<NoteEntity> var body: some View {
    List(notes) { n in
      VStack(alignment: .leading) {
        Text(n.title ?? "Untitled").font(.headline)
        Text(n.body ?? "").font(.subheadline)
      }
    }
    .toolbar { Button("Add") { add() } }
    } private func add() {
    let n = NoteEntity(context: context)
    n.id = UUID(); n.title = "New Note"; n.body = ""
    try? context.save()
    } }

    App Group Shared Store (Widget Access)

    Store your Core Data SQLite file in an App Group so your Widget can read the same data.

    This example demonstrates how to set up an App Group shared store.

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import CoreData
    
    struct PersistenceController {
      static let shared = PersistenceController()
      let container: NSPersistentContainer
      init() {
    
    container = NSPersistentContainer(name: "MyModel")
    if let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.notes") {
      let storeURL = groupURL.appendingPathComponent("MyModel.sqlite")
      let description = NSPersistentStoreDescription(url: storeURL)
      container.persistentStoreDescriptions = &#91;description]
    }
    container.loadPersistentStores { _, error in
      if let error = error { fatalError("Unresolved error: \(error)") }
    }
    } } struct AppGroupListView: View { @Environment(\.managedObjectContext) private var context @FetchRequest(sortDescriptors: []) private var items: FetchedResults<Item> var body: some View {
    List(items) { item in Text(item.title ?? "") }
      .toolbar { Button("Add") { add() } }
    } private func add() {
    let i = Item(context: context)
    i.title = "Shared Item"
    try? context.save()
    } }

    Enable the same App Group on the Widget target, then query via a Core Data stack that points to the same container URL.

  • Persistence

    UserDefaults API

    Store small preferences like flags or last-used values.

    Syntax:

    • UserDefaults.standard.set(_:forKey:)
    • .bool(forKey:)
    • SwiftUI @AppStorage("key")

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    
    struct UserDefaultsDemo: View {
      @AppStorage("username") private var username = ""
      var body: some View {
    
    VStack(spacing: 12) {
      Text(username.isEmpty ? "Hello, Guest" : "Hello, " + username)
      TextField("Username", text: $username)
        .textFieldStyle(.roundedBorder)
    }
    .padding()
    } }

    The UserDefaults API provides a simple way to store small preferences and settings, such as flags or last-used values.

    Tip: For larger data or sync, use Core Data or CloudKit.

    Avoid storing secrets in UserDefaults.

    App Group Shared Defaults (Widget Access)

    Share small values between your app and a Widget (or extensions) using an App Group suite.

    Syntax:

    • UserDefaults(suiteName: "group.id")
    • shared.set(_:forKey:)
    • shared.integer(forKey:)

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import Foundation
    
    struct AppGroupDemo: View {
      private let suite = UserDefaults(suiteName: "group.com.example.notes")!
      @State private var count: Int = 0
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Notes count: \(count)")
      HStack {
        Button("Increment") { count += 1; suite.set(count, forKey: "notesCount") }
        Button("Load") { count = suite.integer(forKey: "notesCount") }
        Button("Clear") { suite.removeObject(forKey: "notesCount"); count = 0 }
      }
    }
    .task { count = suite.integer(forKey: "notesCount") }
    .padding()
    } }
  • Networking

    GET Request with async/await

    Use URLSession.shared.data(from:) with async/await to fetch data.

    Syntax:

    • let (data, response) = try await URLSession.shared.data(from: url)
    • decode with JSONDecoder().decode

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import Foundation
    
    struct Todo: Decodable, Identifiable { let id: Int; let title: String }
    
    func fetchTodos() async throws -> [Todo] {
      let url = URL(string: "https://jsonplaceholder.typicode.com/todos?_limit=2")!
      let (data, _) = try await URLSession.shared.data(from: url)
      return try JSONDecoder().decode([Todo].self, from: data)
    }
    
    struct NetworkingGetDemo: View {
      @State private var todos: [Todo] = []
      var body: some View {
    
    List(todos) { t in Text(t.title) }
      .task {
        do { todos = try await fetchTodos() } catch { print(error) }
      }
    } }

    This example fetches a small JSON list with URLSession using async/await and displays it in a SwiftUI List.


    Sample App: Notes (Networking)

    Use @MainActor class VM: ObservableObject { @Published var items: [T] = []; func loadFromAPI() async { ... } } to separate concerns and load data in the background.

    Syntax:

    • @MainActor class VM: ObservableObject { @Published var items: [T] = []; func loadFromAPI() async { ... } }
    • View: .task { await vm.loadFromAPI() } to kick off the async load
    • API: let (data, _) = try await URLSession.shared.data(from: url)JSONDecoder().decode

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import Foundation
    
    struct NoteDTO: Decodable, Identifiable { let id: Int; let title: String; let body: String }
    
    enum API {
      static func fetchNotes() async throws -> [NoteDTO] {
    
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts?_limit=3")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(&#91;NoteDTO].self, from: data)
    } } @MainActor final class NotesViewModel: ObservableObject { @Published var notes: [NoteDTO] = [] func loadFromAPI() async {
    do { notes = try await API.fetchNotes() }
    catch { print(error) }
    } } struct NotesView: View { @StateObject private var vm = NotesViewModel() var body: some View {
    List(vm.notes) { n in Text(n.title) }
      .task { await vm.loadFromAPI() }
    } }

    This example separates concerns with an API helper and a ViewModel, then loads notes into a SwiftUI List on task.


    POST JSON

    Send JSON by encoding your payload and setting the request method and headers.

    Syntax:

    • var req = URLRequest(url:)
    • req.httpMethod = "POST"
    • req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    • req.httpBody = try JSONEncoder().encode(payload)
    • let (_, resp) = try await URLSession.shared.data(for: req)

    Example

    Demo.swift

    ContentView.swift

    App.swift

    import SwiftUI
    import Foundation
    
    struct NewTodo: Encodable { let title: String; let completed: Bool }
    
    func createTodo(_ todo: NewTodo) async throws -> Int {
      let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
      var req = URLRequest(url: url)
      req.httpMethod = "POST"
      req.setValue("application/json", forHTTPHeaderField: "Content-Type")
      req.httpBody = try JSONEncoder().encode(todo)
      let (_, resp) = try await URLSession.shared.data(for: req)
      return (resp as? HTTPURLResponse)?.statusCode ?? 0
    }
    
    struct NetworkingPostDemo: View {
      @State private var status: Int? = nil
      var body: some View {
    
    VStack(spacing: 12) {
      Button("Create Todo") {
        Task {
          do { status = try await createTodo(NewTodo(title: "Demo", completed: false)) }
          catch { print(error); status = nil }
        }
      }
      if let status { Text("Status: \(status)") }
    }
    .padding()
    } }

    This example sends a JSON payload with a POST request and returns the HTTP status code from the response.