[譯]理解 SwiftUI 里的屬性裝飾器@State, @Binding, @ObservedObject, @EnvironmentObject


原文地址:https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/

@States

通過使用 @State 修飾器我們可以關聯出 View 的狀態. SwiftUI 將會把使用過 @State 修飾器的屬性存儲到一個特殊的內存區域,並且這個區域和 View struct 是隔離的. 當 @State 裝飾過的屬性發生了變化,SwiftUI 會根據新的屬性值重新創建視圖

struct ProductsView: View {
    let products: [Product]

    @State private var showFavorited: Bool = false

    var body: some View {
        List {
            Button(
                action: { self.showFavorited.toggle() },
                label: { Text("Change filter") }
            )

            ForEach(products) { product in
                if !self.showFavorited || product.isFavorited {
                    Text(product.title)
                }
            }
        }
    }
}

這個例子里我們創建了一個列表,點擊按鈕 showFavorited 會發生值的取反操作,然后 SwiftUI 會通過最新的值更新值

譯者:這個 demo 在最新的 xcode 11 beta 6 中已經無法運行起來了,因為 Button 組件的語法已經修改了

@Binding

有時候我們會把一個視圖的屬性傳至子節點中,但是又不能直接的傳遞給子節點,因為在 Swift 中值的傳遞形式是值類型傳遞方式,也就是傳遞給子節點的是一個拷貝過的值。但是通過 @Binding 修飾器修飾后,屬性變成了一個引用類型,傳遞變成了引用傳遞,這樣父子視圖的狀態就能關聯起來了。

struct FilterView: View {
    @Binding var showFavorited: Bool

    var body: some View {
        Toggle(isOn: $showFavorited) {
            Text("Change filter")
        }
    }
}

struct ProductsView: View {
    let products: [Product]

    @State private var showFavorited: Bool = false

    var body: some View {
        List {
            FilterView(showFavorited: $showFavorited)

            ForEach(products) { product in
                if !self.showFavorited || product.isFavorited {
                    Text(product.title)
                }
            }
        }
    }
}

我們在 FilterView 視圖里用 @Binding 修飾 showFavorited 屬性, 在傳遞屬性是使用 $ 來傳遞 showFavorited 屬性的引用,這樣 FilterView 視圖就能讀寫父視圖 ProductsView 里的狀態值了,並且值發生了修改 SwiftUI 會更新 ProductsView 和 FilterView 視圖

譯者:在 FilterView 視圖里,Toggle 組件的創建也使用 $showFavorited 這種格式,因為 Toggle 組件會修改傳入的值,如果是一個純讀的組件比如 Text 就不需要 使用 $showFavorited, 直接 Text(showFavorited) 使用就好了

@ObservedObject

@ObservedObject 的用處和 @State 非常相似,從名字看來它是來修飾一個對象的,這個對象可以給多個獨立的 View 使用。如果你用 @ObservedObject 來修飾一個對象,那么那個對象必須要實現 ObservableObject 協議,然后用 @Published 修飾對象里屬性,表示這個屬性是需要被 SwiftUI 監聽的

final class PodcastPlayer: ObservableObject {
    @Published private(set) var isPlaying: Bool = false

    func play() {
        isPlaying = true
    }

    func pause() {
        isPlaying = false
    }
}

我們定義了一個 PodcastPlayer 類,這個類可以給不同的 View 使用,SwiftUI 會追蹤使用 View 里經過 @ObservableObject 修飾過的對象里進過 @Published 修飾的屬性變換,一旦發生了變換,SwiftUI 會更新相關聯的 UI

struct EpisodesView: View {
    @ObservedObject var player: PodcastPlayer
    let episodes: [Episode]

    var body: some View {
        List {
            Button(
                action: {
                    if self.player.isPlaying {
                        self.player.pause()
                    } else {
                        self.player.play()
                    }
            }, label: {
                    Text(player.isPlaying ? "Pause": "Play")
                }
            )
            ForEach(episodes) { episode in
                Text(episode.title)
            }
        }
    }
}

譯者:這個 demo 在最新的 xcode 11 beta 6 中已經無法運行起來了,因為 Button 組件的語法已經修改了

@EnvironmentObject

從名字上可以看出,這個修飾器是針對全局環境的。通過它,我們可以避免在初始 View 時創建 ObservableObject, 而是從環境中獲取 ObservableObject

SceneDelegate.swift 文件

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        let episodes = [
            Episode(id: 1, title: "First episode"),
            Episode(id: 2, title: "Second episode")
        ]

        let player = PodcastPlayer()
        window.rootViewController = UIHostingController(
            rootView: EpisodesView(episodes: episodes)
                .environmentObject(player)
        )
        self.window = window
        window.makeKeyAndVisible()
    }
}

EpisodesView.swift 文件

struct EpisodesView: View {
    @EnvironmentObject var player: PodcastPlayer
    let episodes: [Episode]

    var body: some View {
        List {
            Button(
                action: {
                    if self.player.isPlaying {
                        self.player.pause()
                    } else {
                        self.player.play()
                    }
            }, label: {
                    Text(player.isPlaying ? "Pause": "Play")
                }
            )
            ForEach(episodes) { episode in
                Text(episode.title)
            }
        }
    }
}

可以看出我們獲取 PodcastPlayer 這個 ObservableObject 是通過 @EnvironmentObject 修飾器,但是在入口需要傳入 .environmentObject(player) 。@EnvironmentObject 的工作方式是在 Environment 查找 PodcastPlayer 實例。

@Environment

繼續上面一段的說明,我們的確開一個從 Environment 拿到用戶自定義的 object,但是 SwiftUI 本身就有很多系統級別的設定,我們開一個通過 @Environment 來獲取到它們

struct CalendarView: View {
    @Environment(\.calendar) var calendar: Calendar
    @Environment(\.locale) var locale: Locale
    @Environment(\.colorScheme) var colorScheme: ColorScheme

    var body: some View {
        return Text(locale.identifier)
    }
}

通過 @Environment 修飾的屬性,我們開一個監聽系統級別信息的變換,這個例子里一旦 Calendar, Locale, ColorScheme 發生了變換,我們定義的 CalendarView 就會刷新

謝謝


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM