肘子的 Swift 记事本

Exploring Property Wrappers in SwiftUI: @UIApplicationDelegateAdaptor, @AccessibilityFocusState, @FocusedObject, @FocusedValue, and @FocusedBinding

Published on

Get weekly handpicked updates on Swift and SwiftUI!

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.

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

Swift
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.
Swift
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:
Swift
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 implements ObservableObject and AppDelegate is injected into the environment, SwiftUI will also automatically inject SceneDelegate into the same environment.
Swift
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.
Swift
@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 the scenePhase environment value.
Swift
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, and userActivity, 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

Swift
// 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.
Swift
@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):
Swift
// 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.
Swift
// 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 the ObservableObject 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 multiple focusedObject or focusedSceneObject 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 through focusedObject when they gain focus.
Swift
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 through focusedObject or focusedSceneObject 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 through focusedObject or focusedSceneObject.
Swift
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, whichever TextEditor gains focus, @FocusedObject will provide the relevant data:
Swift
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.
Swift
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:
Swift
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:

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

Swift
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 extending FocusedValues, 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 no focusedSceneValue 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):

Swift
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 be Binding.
  • Dedicated for Value Type Data: @FocusedBinding is only for value type data. Once the issues with @Observable are resolved, it will be unnecessary to use Binding 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.

I'm really looking forward to hearing your thoughts! Please Leave Your Comments Below to share your views and insights.

Fatbobman(东坡肘子)

I'm passionate about life and sharing knowledge. My blog focuses on Swift, SwiftUI, Core Data, and Swift Data. Follow my social media for the latest updates.

You can support me in the following ways