In this article, we will explore property wrappers such as @UIApplicationDelegateAdaptor
, @AccessibilityFocusState
, @FocusedObject
, @FocusedValue
, and @FocusedBinding
. These property wrappers cover various functionalities including integration across different framework lifecycles, assistive focus, and management of focused value observations.
This article aims to provide an overview of the main functions and usage considerations of these property wrappers, rather than a detailed usage guide.
- @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Environment
- @AppStorage, @SceneStorage, @FocusState, @GestureState, @ScaledMetric
- @FetchRequest, @SectionedFetchRequest, @Query, @Namespace, @Bindable
1. @UIApplicationDelegateAdaptor
The @UIApplicationDelegateAdaptor
provides developers with the ability to access and utilize the AppDelegate functionalities of UIKit within applications based on the SwiftUI lifecycle. This enables handling tasks specific to UIKit, such as push notifications and lifecycle events.
1.1 Basic Usage
class AppDelegate: NSObject, UIApplicationDelegate {
// Implement relevant UIApplicationDelegate methods
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("App launched")
return true
}
}
@main
struct DelegateDemo: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
In the code example above, we first declare a class that conforms to both NSObject
and UIApplicationDelegate
protocols. Then, by using UIApplicationDelegateAdaptor
, we register this class within the app’s declaration.
1.2 Main Functions
- Allows SwiftUI applications to leverage the rich functionalities provided by UIKit, such as background task handling and app lifecycle management.
- For existing UIKit applications, using
@UIApplicationDelegateAdaptor
can facilitate a smoother transition to SwiftUI without the need to rewrite a significant amount of application logic.
1.3 Considerations and Tips
- Uniqueness and Positioning Restrictions: The
UIApplicationDelegateAdaptor
should be defined within the main body declaration of the App and can only be defined once throughout the entire application. - Environment Variable Injection: The class handling AppDelegate logic can implement the
ObservableObject
protocol and be injected into the view hierarchy as an environment variable. This allows for the instance of AppDelegate to be accessed within views using@EnvironmentObject
.
class AppDelegate: NSObject,UIApplicationDelegate,ObservableObject {
@Published var launched:Bool = false
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
launched = true
return true
}
}
@main
struct DelegateDemo: App {
@UIApplicationDelegateAdaptor var delegate:AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(delegate)
}
}
}
struct ContentView:View {
@EnvironmentObject var delegate:AppDelegate
var body: some View {
Text("Launched \(delegate.launched ? "True" : "False")")
}
}
- Focus on Lifecycle and System Events: It is recommended that the
AppDelegate
class focuses on handling application lifecycle and system events, avoiding the incorporation of business logic. - Handling SceneDelegate Events: To respond to events from
UIWindowSceneDelegate
, the following implementation can be utilized:
final class SceneDelegate:NSObject,UIWindowSceneDelegate{
func sceneWillEnterForeground(_ scene: UIScene) {
print("will enter foreground")
}
}
extension AppDelegate {
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = SceneDelegate.self
return sceneConfig
}
}
- Automatic Injection of SceneDelegate: If
SceneDelegate
implementsObservableObject
andAppDelegate
is injected into the environment, SwiftUI will also automatically injectSceneDelegate
into the same environment.
extension SceneDelegate:ObservableObject {}
ContentView()
.environmentObject(delegate)
struct ContentView1:View {
@EnvironmentObject var appDelegate:AppDelegate
@EnvironmentObject var sceneDelegate:SceneDelegate
var body: some View {
Text("Launched \(appDelegate.launched ? "True" : "False")")
}
}
- Applicability with Observation Frameworks: The above logic is equally applicable when using observation frameworks.
@Observable // Using Observation
class AppDelegate: NSObject,UIApplicationDelegate {
var launched:Bool = false
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
launched = true
print("lanched")
return true
}
}
@Observable // Using Observation
final class SceneDelegate:NSObject,UIWindowSceneDelegate{
var foreground:Bool = false
func sceneWillEnterForeground(_ scene: UIScene) {
foreground = true
print("will enter foreground")
}
}
extension AppDelegate {
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = SceneDelegate.self
return sceneConfig
}
}
@main
struct PropertyWrapperApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView1()
.environment(delegate) // inject by Observation way
}
}
}
struct ContentView1:View {
@Environment(AppDelegate.self) var appDelegate
@Environment(SceneDelegate.self) var sceneDelegate
var body: some View {
VStack {
Text("Launched \(appDelegate.launched ? "True" : "False")")
Text("Foreground \(sceneDelegate.foreground ? "True" : "False")")
}
}
}
- Prefer Native SwiftUI Event Handling: For event handling logic that SwiftUI natively supports, such as
sceneWillEnterForeground
, it is recommended to use native methods first, such as responding to thescenePhase
environment value.
struct ContentView:View {
@Environment(\.scenePhase) var scenePhase
var body: some View {
Text("Hello World")
.onChange(of: scenePhase){ phase in
switch phase {
case .active:
print("active")
case .inactive:
print("inactive")
case .background:
print("background")
@unknown default:
break
}
}
}
}
- Other Native Modifiers: SwiftUI also offers a range of modifiers that can be used to avoid handling certain events in the Delegate, such as
onContinueUserActivity
,backgroundTask
,handlesExternalEvents
,onOpenURL
, anduserActivity
, among others. Where possible, these native SwiftUI methods should be preferred.
@NSApplicationDelegateAdaptor
,@WKApplicationDelegateAdaptor
, and@WKExtensionDelegateAdaptor
are very similar in usage to@UIApplicationDelegateAdaptor
but are tailored for different platforms. Given that the basic principles of these property wrappers are the same, this article will not discuss them separately.
2 @AccessibilityFocusState
@AccessibilityFocusState
in SwiftUI is designed to enhance the accessibility experience. This property wrapper enables developers to more effectively manage and respond to the focus state of accessibility features such as VoiceOver, thereby creating application interfaces that are easier to navigate and use for all users. It is very similar in basic concept and application method to @FocusState
, and can be considered a @FocusState
specifically for accessibility elements.
2.1 Basic Usage
// Method 1:
struct AccessibilityFocusStateView: View {
@AccessibilityFocusState(for: .switchControl) var isClickButtonFocused: Bool
var body: some View {
VStack {
Button("Press me") {
print("Pressed")
}
Button("Click me") {
print("Clicked")
}
.accessibilityFocused($isClickButtonFocused)
}
.onChange(of: isClickButtonFocused) {
print($0)
}
}
}
// Method 2:
struct AccessibilityFocusStateView: View {
@AccessibilityFocusState var focused: FocusField?
var body: some View {
VStack {
Button("Press me") {
// do something
// then change focus
focused = .click
}
.accessibilityFocused($focused, equals: .press)
Button("Click me") {
print("Click")
}
.accessibilityFocused($focused, equals: .click)
}
}
}
enum FocusField: Hashable {
case press
case click
}
2.2 Considerations and Tips
- Specific Accessibility Mode Configuration:
@AccessibilityFocusState
can be configured as needed to activate only in specific accessibility modes, such as.switchControl
or.voiceOver
. By default, it supports all accessibility features.
@AccessibilityFocusState(for: .switchControl) var focused: FocusField?
// or
@AccessibilityFocusState(for: .voiceOver) var focused: FocusField?
- Accessibility Testing: To ensure a good experience for accessibility users, focus management features in the app should be tested with accessibility tools such as VoiceOver to ensure they work as expected.
- Avoid Overcomplication: When using
@AccessibilityFocusState
, avoid introducing overly complex focus management logic in unnecessary scenarios, as this can cause confusion or frustration for users.
3 @FocusedObject
@FocusedObject
is used for observing observable type data provided by the currently focused view or scene. This data can be provided and managed by an observable view that gains focus (using the .focusedObject
modifier) or by a focused scene (using the .focusedSceneObject
modifier).
3.1 Basic Usage
- Observing Focused Scene Data: The following code creates a menu item called Empty on macOS, which clears the content of the text box in the currently focused scene (use ⌘-N to create a new scene):
// Requires conforming to the ObservableObject protocol
class DataModel: ObservableObject {
@Published var text = ""
}
struct MyCommands: Commands {
// Retrieve the instance of DataModel provided by focusedSceneObject in the current focused scene, or nil if none
@FocusedObject var dataModel: DataModel?
var body: some Commands {
CommandMenu("Action") {
Button("Empty") {
dataModel?.text = ""
}
}
}
}
@main
struct FocusedSceneObjectDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.scenePadding()
}
.commands {
MyCommands()
}
}
}
struct ContentView: View {
@StateObject var dataModel = DataModel()
var body: some View {
VStack {
// focusedSceneObject will automatically provide data when the scene is focused, regardless of whether the current view is focusable
Text("Input:")
.focusedSceneObject($dataModel) // Correction: Use $dataModel for binding
TextEditor(text: $dataModel.text)
}
}
}
This example demonstrates how to use @FocusedObject
to observe and modify data in a specific focus context, such as a scene in a macOS application. By using @FocusedObject
, developers can create more interactive and responsive apps that react to changes in user focus within the app.
- Observing Data from a Focused View (Focusable View): The following code provides the same functionality as the above, the only difference being that
focusedObject
is applied to a focusable view or element, and data is only provided once that view or element gains focus.
// The rest of the code remains the same
struct ContentView: View {
@StateObject var dataModel = DataModel()
var body: some View {
VStack {
Text("Input:")
// Data is provided by a focusable view when it gains focus
TextEditor(text: $dataModel.text)
.focusedObject($dataModel) // Correction: Use $dataModel for binding
}
}
}
3.2 Main Functions
@FocusedObject
offers applications the ability to obtain observable objects from the currently focused elements, greatly facilitating developers in creating dynamic and responsive interfaces to user actions. It is frequently used to implement features that require processing based on the current focus state, such as menus and hubs in various scenarios.
3.3 Considerations and Tips
- Optional Value Type:
@FocusedObject
should be declared as an optional (Optional) type. The value of this object will automatically become nil when the related focus scene or element loses focus. - Conform to
ObservableObject
: The data type used for@FocusedObject
must implement theObservableObject
protocol. - Unique Instance Data: Similar to
EnvironmentObject
, for any given type, the system retains only one instance’s data at a time. Therefore, you should avoid using multiplefocusedObject
orfocusedSceneObject
instances to provide multiple sets of data of the same type simultaneously. - Multi-Scene Data Provision: In multi-scene applications, it’s recommended to use
focusedSceneObject
to provide data across scenes. - Making Elements Focusable: Elements that are not natively focusable can be made focusable with the
focusable
modifier, thus allowing them to provide data throughfocusedObject
when they gain focus.
Text("Input:")
.focusable()
.focusedObject($dataModel) // Correction: Use $dataModel for binding
- Observation Scope Depends on Declaration Location: The data observed by
@FocusedObject
is influenced by its declaration location. When declared at the App or Commands level, it can access data of the same type provided throughfocusedObject
orfocusedSceneObject
across all scenes. However, if declared within the code for a specific scene, it can only access data of the same type provided by any view within the current scene throughfocusedObject
orfocusedSceneObject
.
struct MyCommands: Commands {
// Can observe DataModel focusable data across all scenes
@FocusedObject var dataModel: DataModel?
var body: some Commands {
....
}
}
@main
struct FocusedSceneObjectDemoApp: App {
// Can observe DataModel focusable data across all scenes
@FocusedObject var dataModel: DataModel?
var body: some Scene {
....
}
}
struct ContentView: View {
// Only observes DataModel focusable data within the current scene
@FocusedObject var dataModel: DataModel?
var body: some View {
....
}
}
- Automatic Detection of Focusable Elements: In addition to being directly applied to specific focusable elements,
@FocusedObject
can also automatically recognize all focusable elements within a view. In the following example, whicheverTextEditor
gains focus,@FocusedObject
will provide the relevant data:
VStack {
TextEditor(text: $dataModel.text)
TextEditor(text: $dataModel.text2)
}
.focusedObject(dataModel)
- Multiple Focuses within a Single Scene:
@FocusedObject
is not only suitable for observing data across multiple scenes but can also be effective within a single scene. For instance, the code below shows how to manage different focus states for users and products within the same interface.
struct MultiFocusedDemo:View {
@StateObject var user = UserProfile()
@StateObject var product = ProductDetails()
var body: some View {
Form {
UserView()
ProductView()
Group {
TextField("User Name:",text:$user.username)
TextField(value: $user.age, format: .number){ Text("Age:")}
}
.focusedObject(user)
Group{
TextField("Product Name:",text:$product.productName)
TextField(value: $product.price, format: .number){ Text("Price:")}
}
.focusedObject(product)
}
}
}
class UserProfile: ObservableObject {
@Published var username: String = "JohnDoe"
@Published var age: Int = 30
}
class ProductDetails: ObservableObject {
@Published var productName: String = "Widget"
@Published var price: Double = 19.99
}
struct UserView: View {
@FocusedObject var user: UserProfile?
var body: some View {
if let userProfile = user {
Text("Username: \(userProfile.username)")
Text("Age: \(userProfile.age)")
}
}
}
struct ProductView: View {
@FocusedObject var product: ProductDetails?
var body: some View {
if let productDetails = product {
Text("Product: \(productDetails.productName)")
Text("Price: $\(productDetails.price)")
}
}
}
- Combining with Other Focus Management Solutions:
@FocusedObject
can be combined with other focus management tools for more flexible interaction designs. For example, in the code below, we use@FocusState
to achieve immediate access to the relevant observable object data as an element gains focus:
struct FocusStateDemo:View {
@FocusState var focused:Bool
@FocusedObject var data:DataModel?
@StateObject var model = DataModel()
var body: some View {
VStack {
if let text = data?.text {
Text(text)
}
TextField("",text:$model.text)
.focused($focused)
.focusedObject(model)
}
.task {
focused = true
}
}
}
4 @FocusedValue
The @FocusedValue
property wrapper in SwiftUI serves a similar purpose to @FocusedObject
, but it focuses on value types and observable object instances built on the Observation framework (using @Observable
).
4.1 Basic Usage
Similar to EnvironmentValue, using @FocusedValue
requires declaring a FocusedValueKey
and extending FocusedValues
:
struct MyFocusKey: FocusedValueKey {
typealias Value = String
}
extension FocusedValues {
var myKey: String? { // Optional
get { self[MyFocusKey.self] }
set { self[MyFocusKey.self] = newValue }
}
}
When it loses focus, the system resets @FocusedValue
, so when declaring a FocusedValueKey
, the default value is nil (no need to set a default value).
In application, the usage of @FocusedValue
is similar to @FocusedObject
:
struct ContentView: View {
@FocusedValue(\.myKey) var key
var body: some View {
VStack {
Text(key ?? "nil")
SubView()
}
}
}
struct SubView:View {
@State var key = "Hello"
var body: some View {
TextField("text",text:$key)
.focusedValue(\.myKey, key)
}
}
4.2 Considerations and Tips
- Most of the considerations and tips mentioned for
@FocusedObject
are also applicable to@FocusedValue
. - When declaring
FocusedValueKey
and extendingFocusedValues
, ensure the used type is Optional. - As of Xcode 15.2 version, although
focusedValue
supports sending instances created by@Observable
,@FocusedValue
still cannot properly observe the corresponding instances. Additionally, there is currently nofocusedSceneValue
version that supports Observable instances. It is expected that these issues will be resolved in future versions.
5. @FocusedBinding
The @FocusedBinding
property wrapper gives developers the ability to modify FocusedValueKey
data at the focus value observation end, providing more flexibility and control.
5.1 Basic Usage
@FocusedBinding
allows for direct modification of focus-related binding data within the interface. The following code example demonstrates how to modify data related to myKey
in a text input field and a button (data provision and observation ends):
struct ContentView: View {
@FocusedBinding(\.myKey) var key
var body: some View {
VStack {
Text(key ?? "nil")
Button("Change Key") {
key = "\(Int.random(in: 0..<100))"
}
SubView()
}
}
}
struct SubView: View {
@State var key = "Hello"
var body: some View {
TextField("text", text: $key)
.focusedValue(\.myKey, $key) // Binding
}
}
struct MyFocusKey: FocusedValueKey {
typealias Value = Binding<String> // Binding
}
extension FocusedValues {
var myKey: Binding<String>? { // Optional
get { self[MyFocusKey.self] }
set { self[MyFocusKey.self] = newValue }
}
}
5.2 Considerations and Tips
- Binding Type Declaration: The type declared in
FocusedValueKey
should beBinding
. - Dedicated for Value Type Data:
@FocusedBinding
is only for value type data. Once the issues with@Observable
are resolved, it will be unnecessary to useBinding
to directly modify its properties at the observation end (similar to@FocusedObject
). - Compatibility with SwiftUI Lifecycle: Currently,
@FocusedBinding
is only effective in applications using the SwiftUI lifecycle.
Conclusion
The property wrappers in Swift language and the birth of SwiftUI occurred in the same year. SwiftUI fully leverages this feature, offering developers a series of property wrappers that greatly simplify the development process. In this series of four articles, we have thoroughly reviewed all the property wrappers provided by SwiftUI as of iOS 17, aiming to help developers use SwiftUI more efficiently and conveniently. We hope this content provides valuable guidance and assistance when using SwiftUI.