← Back to list

Property wrappers in SwiftUI

Before we start working on SwiftUI, we should be aware of it’s fundamentals. one of the most fundamental is Property Wrappers.

Keval Gajjar · 2026-08-04 05:50 · 0 claps · 3.6 min read
#swiftui-5 #property-wrapper #combine-framework #swiftdata
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🎵 · Music & Audio

Property wrappers in SwiftUI

Before we start working on SwiftUI, we should be aware of it’s fundamentals. one of the most fundamental is Property Wrappers.

There are categories as follow : Dynamic Types: These are the property wrappers which purely been introduced in SwiftUI (introduced after iOS 13 onwards — 2019).

@State Stores a value owned by the view itself; when it changes, the view redraws.

@Binding A two-way link to a value that lives in a different (usually parent) view.

@StateObject Creates and owns a class-based observable object; survives view redraws.

@ObservedObject Watches a class-based observable object that’s owned somewhere else (you don’t create it here). (Note: this is the real name — “ObservableObject” is the protocol, not this wrapper.)

@EnvironmentObject Grabs a shared observable object that was placed into the environment by an ancestor view.

@Environment Reads a system or custom value from the environment (like color scheme, dismiss action, etc.).

@FocusState Tracks/controls which input field currently has keyboard focus.

@GestureState Holds temporary state during a gesture; automatically resets when the gesture ends.

@ScaledMetric Auto-adjusts a number (like spacing or size) based on the user’s Dynamic Type text size.

@AppStorage Saves/reads a small value directly to UserDefaults (persists across app launches).

@SceneStorage Restores small UI state (like scroll position) when a scene is recreated.

@FetchRequest Fetches and live-observes a list of Core Data objects.

@SectionedFetchRequest Same as above, but groups the results into sections.

@Query Fetches and live-observes model data in SwiftData (the newer replacement for Core Data).

@Bindable Lets you create two-way bindings to properties of an @Observable class (used with the newer Observation framework / SwiftData).

Protocol (not a property wrapper)

@ObsevableObject — A protocol you make a class conform to, so SwiftUI can watch its @Published properties for changes.

Other

@Namespace — Creates a shared ID space so SwiftUI can animate matching views smoothly between states (matched geometry effect).

Example

import SwiftUI
import CoreData
import SwiftData

// MARK: - ObservableObject (protocol) + @Published
// A reference-type "model" SwiftUI can watch. This is the protocol
// that makes @StateObject / @ObservedObject / @EnvironmentObject work.
class NotesSettings: ObservableObject {
    @Published var isDarkNotesTheme: Bool = false
}

// MARK: - @Observable class (new Observation framework, used with @Bindable)
@Observable
class Draft {
    var text: String = ""
}

// MARK: - Root screen
struct NotesScreen: View {

    @StateObject private var settings = NotesSettings()
    @State private var newNoteTitle: String = ""
    @FocusState private var titleFieldIsFocused: Bool
    @GestureState private var dragOffset: CGFloat = 0
    @ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24
    @AppStorage("showCompletedNotes") private var showCompletedNotes: Bool = true
    @SceneStorage("notesScreen.selectedTab") private var selectedTab: Int = 0
    @Namespace private var noteAnimation
    @Bindable var draft = Draft()

    var body: some View {
        NavigationStack {
            VStack(spacing: 16) {

                TextField("New note title", text: $newNoteTitle)
                    .focused($titleFieldIsFocused)
                    .textFieldStyle(.roundedBorder)
                    .font(.system(size: iconSize))

                TextField("Draft body (Observable + Bindable)", text: $draft.text)
                    .textFieldStyle(.roundedBorder)

                Toggle("Show completed notes", isOn: $showCompletedNotes)
                Toggle("Dark notes theme", isOn: $settings.isDarkNotesTheme)

                Rectangle()
                    .fill(Color.gray.opacity(0.4))
                    .frame(width: 60, height: 20)
                    .offset(x: dragOffset)
                    .gesture(
                        DragGesture()
                            .updating($dragOffset) { value, state, _ in
                                state = value.translation.width
                            }
                    )
                    .matchedGeometryEffect(id: "handle", in: noteAnimation)

                Picker("Source", selection: $selectedTab) {
                    Text("Core Data").tag(0)
                    Text("SwiftData").tag(1)
                }
                .pickerStyle(.segmented)

                if selectedTab == 0 {
                    CoreDataNotesList()
                } else {
                    SwiftDataNotesList()
                }
            }
            .padding()
            .navigationTitle("Notes")
            .onAppear { titleFieldIsFocused = true }
            .environmentObject(settings)
        }
    }
}

// MARK: - A child that just watches an object owned elsewhere
struct NoteSummaryBadge: View {
    @ObservedObject var settings: NotesSettings

    var body: some View {
        Text(settings.isDarkNotesTheme ? "🌙" : "☀️")
    }
}

// MARK: - A deeper child pulling settings from the environment
struct FooterView: View {
    @EnvironmentObject var settings: NotesSettings
    @Environment(\.dismiss) var dismiss
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        HStack {
            Text(colorScheme == .dark ? "Dark mode" : "Light mode")
            Button("Close") { dismiss() }
        }
    }
}

// MARK: - A reusable view driven purely by a @Binding
struct TitleEditor: View {
    @Binding var title: String

    var body: some View {
        TextField("Title", text: $title)
    }
}

// MARK: - Core Data example
struct CoreDataNotesList: View {
    @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)])
    private var notes: FetchedResults<NoteEntity>

    @SectionedFetchRequest(sectionIdentifier: \NoteEntity.category, sortDescriptors: [])
    private var sectionedNotes: SectionedFetchResults<String, NoteEntity>

    var body: some View {
        List {
            ForEach(sectionedNotes) { section in
                Section(header: Text(section.id)) {
                    ForEach(section) { note in
                        Text(note.title ?? "Untitled")
                    }
                }
            }
        }
    }
}

// Placeholder Core Data entity (normally generated from your .xcdatamodeld)
@objc(NoteEntity)
class NoteEntity: NSManagedObject {
    @NSManaged var title: String?
    @NSManaged var category: String
    @NSManaged var createdAt: Date
}

// MARK: - SwiftData example
struct SwiftDataNotesList: View {
    @Query(sort: \SwiftDataNote.createdAt, order: .reverse)
    private var notes: [SwiftDataNote]

    var body: some View {
        List(notes) { note in
            Text(note.title)
        }
    }
}

@Model
class SwiftDataNote {
    var title: String
    var createdAt: Date
    init(title: String, createdAt: Date = .now) {
        self.title = title
        self.createdAt = createdAt
    }
}

Walking through it

  • **@State / @Binding** — newNoteTitle is owned locally with @State. TitleEditor takes a @Binding so a child view can edit a value it doesn't own.
  • **@StateObject / @ObservedObject / @EnvironmentObject** — NotesScreen creates settings with @StateObject because it's the owner. NoteSummaryBadge receives the same object as @ObservedObject because it only watches. FooterView pulls it from the environment instead of being passed it directly.
  • **@Environment** — used for dismiss and colorScheme, both built-in system values.
  • **@FocusState / @GestureState** — focus for the title field, a transient offset for the drag handle.
  • **@ScaledMetric / @AppStorage / @SceneStorage** — a font size that respects accessibility settings, a toggle persisted to UserDefaults, and a tab selection that survives the scene being recreated.
  • **@FetchRequest / @SectionedFetchRequest** — Core Data, flat versus grouped by category.
  • **@Query** — the SwiftData equivalent of @FetchRequest.
  • **@Observable + @Bindable** — Draft uses the newer Observation framework instead of ObservableObject/@Published; @Bindable lets you bind directly into its properties.
  • **@Namespace** — ties the drag handle to a matchedGeometryEffect ID space for smooth animation.

메타데이터
post_id
a00983c3d23d
slug
property-wrappers-in-swiftui-a00983c3d23d
url
https://medium.com/@kevalunchadiya92/property-wrappers-in-swiftui-a00983c3d23d
canonical_url
https://medium.com/@kevalunchadiya92/property-wrappers-in-swiftui-a00983c3d23d
author_url
https://medium.com/@kevalunchadiya92
status
ok
fetched_at
2026-09-02 06:49:20