In this article, we will continue to explore property wrappers in SwiftUI: @AppStorage, @SceneStorage, @FocusState, @GestureState, and @ScaledMetric. These property wrappers cover various aspects including data persistence, interactive response, accessibility features, and multi-window support, providing developers with succinct and practical solutions.
This article aims to provide an overview of the main functionalities and usage considerations of these property wrappers, rather than an exhaustive guide.
- @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Environment
- @FetchRequest, @SectionedFetchRequest, @Query, @Namespace, @Bindable
- @UIApplicationDelegateAdaptor, @AccessibilityFocusState, @FocusedObject, @FocusedValue, @FocusedBinding
1. @AppStorage
In SwiftUI, @AppStorage is a property wrapper primarily used for data persistence. It allows us to easily store small amounts of data in the user’s default settings (UserDefaults). Additionally, when this data changes, the associated views are automatically updated.
1.1 Basic Usage
Here is a basic example of using @AppStorage:
@AppStorage("isLogin") var isLogin: Bool = false1.2 Main Functions
- @AppStorageis primarily used for storing and retrieving data that is used globally across the application, such as user preferences, last visit time, number of visits, etc.
- Through UserDefaults, @AppStorageachieves persistent data storage, ensuring that data remains saved even after the application is closed.
- When the corresponding values in UserDefaults change, @AppStorageautomatically updates the view, ensuring that the data stays in sync with the interface.
1.3 Considerations and Tips
- The persistence of UserDefaults is not atomic, which means there is a risk of data loss. Therefore, it’s not recommended to use @AppStoragefor storing critical data that, if lost, could affect the normal operation of the app.
- Similarly, it’s not advisable to use @AppStoragefor storing sensitive data.
- @AppStorage, as a SwiftUI wrapper for UserDefaults, by default only supports a limited range of data types. Common data types like dates and arrays are not supported by default. Developers can enable storage for more types by conforming unsupported data types to the- RawRepresentableprotocol. For more information, refer to: Mastering @AppStorage in SwiftUI.
- Ensure the data stored is lightweight. Storing large-sized data in @AppStoragecould lead to performance degradation.
- Besides the default standardsuite,@AppStoragealso supports developer-defined UserDefaults suites. The following code shows how to save data in a suite corresponding to an App Group:
public extension UserDefaults {
    static let appGroup = UserDefaults(suiteName: "group.com.fatbobman.myApp")!
}
@AppStorage("isLogin",store: .appGroup) var isLogin: Bool = false- Using defaultAppStorageallows setting a default UserDefaults suite for the view, avoiding the need to set it repeatedly in each@AppStorage:
ContentView()
    .defaultAppStorage(.appGroup)
@AppStorage("isLogin") var isLogin: Bool = false // in ContentView, store in appGroup suit- The default values set in @AppStorageare only applicable to it and do not apply to direct access to UserDefaults:
@AppStorage("count") var count  = 100
// in View
print(count) // 100
print(UserDefaults.standard.value(forKey: "count")) // nil- Default values set using UserDefaults’ registermethod are applicable to@AppStorage:
struct DefaultValue: View {
    @AppStorage("count") var count = 100
    var body: some View {
        Button("Count") {
            print(count) // 50
        }
    }
}
DefaultValue()
    .onAppear {
        UserDefaults.standard.register(defaults: ["count": 50])
    }- The default values for key-value pairs in @AppStorageare determined by their first set:
@AppStorage("count") var count = 100
@AppStorage("count") var count1 = 300
print(count) // 100- Multiple instances of @AppStoragecan be placed within a class that conforms to theObservableObjectprotocol for unified management. For more information, refer to: Mastering @AppStorage in SwiftUI:
class Settings:ObservableObject {
    @AppStorage("count") var count = 100
    @AppStorage("isLogin") var isLogin = false
}
@StateObject var settings = Settings()
Toggle("Login", isOn: $settings.isLogin)- Similar to UserDefaults, the keys in @AppStorageare string-based. To ensure consistency and avoid issues due to spelling errors in different views, it’s recommended to adopt a unified management approach or define keys uniformly. This practice not only reduces the risk of errors but also makes the code easier to maintain and understand.
enum Keys {
    static let count = "count"
    static let isLogin = "isLogin"
}
@AppStorage(Keys.count) var count = 02. @SceneStorage
@SceneStorage is a property wrapper designed for data sharing within a scene (Scene), mainly applicable to devices supporting multiple scenes, such as iPadOS, macOS, and visionOS. It is capable of saving specific data within each independent scene, making it highly suitable for multi-window or tabbed applications to maintain consistency and persistence in the user interface.
2.1 Basic Usage
@SceneStorage("selectedTab") private var selectedTab: Int = 02.2 Main Functions
@SceneStorage is primarily used for sharing lightweight data across different instances or windows of the same application, such as the user’s selection in a tab or the position of a scroll view.
2.3 Considerations and Tips
- 
The data types supported by @SceneStorageare the same as those supported by@AppStorage, including their type extension methods.
- 
Unlike @AppStorage,@SceneStoragedoes not support a unified management injection method.
- 
@SceneStorageis a unique concept specific to SwiftUI and does not correspond to any known underlying data structure. Therefore, it should only be used within views and not outside of views or in view models.
- 
The data in @SceneStorageis saved independently for each scene and is not shared across different scenes. For cross-scene data sharing,@AppStorageshould be used, or models should be created at the application level.
- 
The working principle of @SceneStorageis similar to that of@State, with the latter being used to save the private state of a view, while@SceneStorageis for saving the private state of a scene. In a sense,@SceneStoragecan be seen as a convenient way to share data between views within a scene, eliminating the need to inject models separately for each scene. For more information on the concept of scenes and how to inject models into different scenes, refer to Building Cross-Platform SwiftUI Apps.
- 
Although @SceneStorageexhibits certain persistent characteristics, the system does not guarantee the specific timing and conditions for data persistence. Especially when a scene is explicitly destroyed (for example, closing an app’s switcher snapshot on iPadOS or closing an app window on macOS), the associated data might be lost. Notably, in practice, even after an app has been explicitly destroyed, the system might still retain the data of the last scene when the app is restarted. However, given the uncertainty of this behavior, it is not recommended to rely on@SceneStorageas the primary means of data persistence.
3. @FocusState
@FocusState is a property wrapper in SwiftUI used for managing focus states. It enables developers to easily track and modify focus states within SwiftUI views.
3.1 Basic Usage
Example using a boolean type:
@FocusState private var isNameFocused: Bool
TextField("name:", text: $name)
    .focused($isNameFocused)Example using an enumeration type:
enum FocusedField: Hashable {
    case name, password
}
@FocusState var focus: FocusedField?
TextField("name:", text: $name)
    .focused($focus, equals: .name)For more detailed usage methods, refer to Advanced SwiftUI TextField - Events, Focus, Keyboard.
3.2 Main Functions
- @FocusStateis primarily used for managing and tracking focus states in the user interface.
- It allows setting a specific input field to be focused by configuring @FocusState.
- @FocusStatecan be used to determine which input field or view element (that has been bound to focus) currently has the focus.
- By binding to certain parts of a view, actions can be executed when a specific element gains or loses focus.
3.3 Considerations and Tips
- Currently, only TextFieldandTextEditsupport changing the focus state via code with@FocusState.
- Search bars created with searchablecannot set or get focus states through@FocusState. Developers with this requirement can refer to the solution provided by Daniel Saidi.
- Before iOS 17, setting the default focus needed to be done in onAppear; from iOS 17 and later versions,defaultFocuscan be used to set the default focus, a feature also applicable to macOS and tvOS.
- In tvOS, @FocusStatecan be used to determine which view currently has the focus.
- Using focusablecan make a view that was originally non-focusable become focusable. For such views, focus can only be obtained through the keyboard (not by setting@FocusStatedirectly), but@FocusStatecan be associated to indicate the current focus state. For example:
struct FocusableDemo: View {
    @FocusState private var isFocused
    var body: some View {
        VStack {
            Rectangle()
                .fill(.red.gradient)
                .overlay(
                    Text("\(isFocused ? "focused" : "")").font(.largeTitle)
                )
                .padding()
                .focusable() // Allows focus
                .focusEffectDisabled() // Disables the default focus style
                .focused($isFocused) // Must be placed after focusable
            Rectangle()
                .fill(.blue.gradient)
                .padding()
                .focusable()
        }
        .padding(50)
    }
}- When using, avoid ambiguity in focus bindings. In the same view, each focus binding should be clear and unique.
4. @GestureState
@GestureState is a property wrapper in SwiftUI designed to simplify gesture handling, primarily used for temporarily storing gesture-related states. These states automatically reset when the gesture activity ends.
4.1 Basic Usage
Below is a basic example of using @GestureState (after the gesture is canceled, isPressed will reset to false):
struct ContentView: View {
    @GestureState var isPressed = false
    var body: some View {
        VStack {
            Rectangle()
                .fill(.orange).frame(width: 200, height: 200)
                .gesture(DragGesture(minimumDistance: 0).updating($isPressed) { _, state, _ in
                    state = true
                })
                .overlay(
                    Text(isPressed ? "Pressing" : "")
                )
        }
    }
}An equivalent method using @State:
struct ContentView: View {
    @State var isPressed = false
    var body: some View {
        VStack {
            Rectangle()
                .fill(.orange).frame(width: 200, height: 200)
                .gesture(DragGesture(minimumDistance: 0).onChanged{ _ in
                    isPressed = true
                }.onEnded{ _ in
                    isPressed = false
                })
                .overlay(
                    Text(isPressed ? "Pressing" : "")
                )
        }
    }
}For more information on SwiftUI gestures, read the article Customizing Gestures in SwiftUI.
- 
4.2 Main Functions- @GestureStateis commonly used for storing temporary gesture data, such as the displacement of a drag or the angle of a rotation.
- It automatically manages the lifecycle of the state, resetting it to its initial value when the gesture ends.
- Using @GestureStatemakes gesture handling code more concise and easier to maintain.
 4.3 Considerations and Tips- @GestureStateis suitable only for temporary, gesture-related states. It is not intended for long-term storage or sharing of state across different parts of an app.
- The constructor of @GestureStateallows setting aTransactionfor the state reset, or determining aTransactionbased on the state value at reset. The following code demonstrates adding animation to the reset operation only when the horizontal movement exceeds 200 units. For more information aboutTransaction, refer to The Secret to Flawless SwiftUI Animations: A Deep Dive into Transactions.
 
struct ContentView: View {
    @GestureState(wrappedValue: CGSize.zero, reset: { value, transaction in
        if abs(value.width) > 200 {
            transaction.animation = .smooth
        }
    }) var offset
    var body: some View {
        VStack {
            Rectangle()
                .fill(.orange).frame(width: 200, height: 200)
                .offset(x: offset.width, y: offset.height)
                .gesture(DragGesture().updating($offset) { value, state, _ in
                    state = value.translation
                })
        }
    }
}- In SwiftUI, certain system operations can interrupt the normal processing of SwiftUI gestures, preventing the onEndedclosure from being executed. Using@GestureStateensures that even if a gesture is interrupted by the system, the related state will still reset to its initial value. For example, in the@Statebased code below, if the user performs a system operation (like pulling down the control center with another hand) during the drag, theoffsetmight not reset. However, with the@GestureStateversion, the state can reset correctly.
struct ContentView: View {
    @State var offset = CGSize.zero
    var body: some View {
        VStack {
            Rectangle()
                .fill(.orange).frame(width: 200, height: 200)
                .offset(x: offset.width, y: offset.height)
                .gesture(DragGesture().onChanged {
                    offset = $0.translation
                }.onEnded { _ in
                    offset = .zero
                })
        }
    }
}5. @ScaledMetric
@ScaledMetric is a property wrapper in SwiftUI for automatically scaling metric values based on the user’s text size settings. It is primarily used to adapt to different users’ accessibility needs, especially in cases where layouts and element sizes need to be adjusted according to the system’s font size settings.
5.1 Basic Usage
Here is a basic example of using @ScaledMetric:
struct ContentView: View {
    @ScaledMetric var size: CGFloat = 100
    var body: some View {
        Image(systemName: "person.fill")
            .frame(width: size, height: size)
    }
}For more specific examples, refer to Mixing Text and Image in SwiftUI.
5.2 Main Functions
- @ScaledMetricis used to automatically adjust values based on the user’s accessibility settings, such as larger text sizes.
- It ensures that the application interface remains usable and comfortable under different user preferences.
- @ScaledMetriccan be used to adjust any dimension that needs to change in proportion to the system font size, such as icon sizes, layout spacing, etc.
5.3 Considerations and Tips
- The relativeToparameter of@ScaledMetricallows associating the value with the size change curve of a
@ScaledMetric(relativeTo: .largeTitle) var height = 17- Different text styles respond differently to dynamic type changes, so their impact on @ScaledMetricis not linear.
- When using @ScaledMetric, it’s important to note that it affects size, not layout structure. Ensure that the application maintains a reasonable layout and functionality at different scaling levels (e.g., combining with ViewThatFits, AnyLayout, GeometryReader, etc.).
- @ScaledMetricis suitable for dynamic size adjustment but should be used cautiously to avoid over-adjusting, which can lead to imbalanced layouts or reduced readability.
- The .dynamicTypeSizecan be used to limit the range of dynamic type size changes for a view, preventing layout anomalies.
Summary
Each property wrapper has its unique application scenarios and considerations. @AppStorage is suitable for lightweight persistence of global data; @SceneStorage focuses on sharing state across scenes; @FocusState simplifies focus management; @GestureState automates the lifecycle of gesture states; @ScaledMetric implements automatic scaling of dimensions.
Using these property wrappers correctly can make SwiftUI code more concise and efficient. Compared to directly using underlying APIs, property wrappers abstract many details, allowing developers to focus more on business logic. Of course, it’s also important to remember their limitations to avoid misuse.
In the future, we will explore more property wrappers that have not yet been introduced.