Exploring SwiftUI Property Wrappers: @AppStorage, @SceneStorage, @FocusState, @GestureState and @ScaledMetric

Published on

Get weekly handpicked updates on Swift and SwiftUI!

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.

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:

Swift
@AppStorage("isLogin") var isLogin: Bool = false

1.2 Main Functions

  • @AppStorage is 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, @AppStorage achieves persistent data storage, ensuring that data remains saved even after the application is closed.
  • When the corresponding values in UserDefaults change, @AppStorage automatically 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 @AppStorage for storing critical data that, if lost, could affect the normal operation of the app.
  • Similarly, it’s not advisable to use @AppStorage for 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 RawRepresentable protocol. For more information, refer to: Mastering @AppStorage in SwiftUI.
  • Ensure the data stored is lightweight. Storing large-sized data in @AppStorage could lead to performance degradation.
  • Besides the default standard suite, @AppStorage also supports developer-defined UserDefaults suites. The following code shows how to save data in a suite corresponding to an App Group:
Swift
public extension UserDefaults {
    static let appGroup = UserDefaults(suiteName: "group.com.fatbobman.myApp")!
}

@AppStorage("isLogin",store: .appGroup) var isLogin: Bool = false
  • Using defaultAppStorage allows setting a default UserDefaults suite for the view, avoiding the need to set it repeatedly in each @AppStorage:
Swift
ContentView()
    .defaultAppStorage(.appGroup)

@AppStorage("isLogin") var isLogin: Bool = false // in ContentView, store in appGroup suit
  • The default values set in @AppStorage are only applicable to it and do not apply to direct access to UserDefaults:
Swift
@AppStorage("count") var count  = 100

// in View
print(count) // 100
print(UserDefaults.standard.value(forKey: "count")) // nil
  • Default values set using UserDefaults’ register method are applicable to @AppStorage:
Swift
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 @AppStorage are determined by their first set:
Swift
@AppStorage("count") var count = 100
@AppStorage("count") var count1 = 300

print(count) // 100
  • Multiple instances of @AppStorage can be placed within a class that conforms to the ObservableObject protocol for unified management. For more information, refer to: Mastering @AppStorage in SwiftUI:
Swift
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 @AppStorage are 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.
Swift
enum Keys {
    static let count = "count"
    static let isLogin = "isLogin"
}

@AppStorage(Keys.count) var count = 0

2. @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

Swift
@SceneStorage("selectedTab") private var selectedTab: Int = 0

2.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 @SceneStorage are the same as those supported by @AppStorage, including their type extension methods.

  • Unlike @AppStorage, @SceneStorage does not support a unified management injection method.

  • @SceneStorage is 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 @SceneStorage is saved independently for each scene and is not shared across different scenes. For cross-scene data sharing, @AppStorage should be used, or models should be created at the application level.

  • The working principle of @SceneStorage is similar to that of @State, with the latter being used to save the private state of a view, while @SceneStorage is for saving the private state of a scene. In a sense, @SceneStorage can 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 @SceneStorage exhibits 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 @SceneStorage as 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:

Swift
@FocusState private var isNameFocused: Bool
TextField("name:", text: $name)
    .focused($isNameFocused)

Example using an enumeration type:

Swift
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

  • @FocusState is 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.
  • @FocusState can 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 TextField and TextEdit support changing the focus state via code with @FocusState.
  • Search bars created with searchable cannot 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, defaultFocus can be used to set the default focus, a feature also applicable to macOS and tvOS.
  • In tvOS, @FocusState can be used to determine which view currently has the focus.
  • Using focusable can make a view that was originally non-focusable become focusable. For such views, focus can only be obtained through the keyboard (not by setting @FocusState directly), but @FocusState can be associated to indicate the current focus state. For example:
Swift
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):

Swift
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:

Swift
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

    • @GestureState is 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 @GestureState makes gesture handling code more concise and easier to maintain.

    4.3 Considerations and Tips

    • @GestureState is 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 @GestureState allows setting a Transaction for the state reset, or determining a Transaction based 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 about Transaction, refer to The Secret to Flawless SwiftUI Animations: A Deep Dive into Transactions.
Swift
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 onEnded closure from being executed. Using @GestureState ensures that even if a gesture is interrupted by the system, the related state will still reset to its initial value. For example, in the @State based code below, if the user performs a system operation (like pulling down the control center with another hand) during the drag, the offset might not reset. However, with the @GestureState version, the state can reset correctly.
Swift
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:

Swift
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

  • @ScaledMetric is 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.
  • @ScaledMetric can 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 relativeTo parameter of @ScaledMetric allows associating the value with the size change curve of a
Swift
@ScaledMetric(relativeTo: .largeTitle) var height = 17
  • Different text styles respond differently to dynamic type changes, so their impact on @ScaledMetric is 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.).
  • @ScaledMetric is suitable for dynamic size adjustment but should be used cautiously to avoid over-adjusting, which can lead to imbalanced layouts or reduced readability.
  • The .dynamicTypeSize can 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.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed