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 = 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 theRawRepresentable
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:
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
:
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:
@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
:
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:
@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 theObservableObject
protocol 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
@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.
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
@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:
@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
@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
andTextEdit
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:
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
@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 aTransaction
for the state reset, or determining aTransaction
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 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
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, theoffset
might not reset. However, with the@GestureState
version, 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
@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
@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.