SwiftUI数据流实践

    发布于 2019-10-19

    前提

    状态(State)是一个 App 中最关键的东西之一,比如我们要用 SwiftUI 来开一个音乐播放器,其中比较关键的是播放按钮,我们会有两个播放状态,播放和暂停,不同状态按钮的图片也是不同的

    struct ContentView: View {
    	private var isPlaying: Bool = false
    
    	var body: some View {
    		Button(action: {
    			...
    		}) {
    			Image(name: isPlaying ? "pause" : "play")
    		}
    	}
    }

    这里我们实现了最简单的根据 isPlaying 状态显示不同按钮图片的代码,但是我们如何通过按钮的点击事件来更新 isPlaying 状态?

    @State

    SwiftUI 通过 @State 来管理属性,当 state 值发生变化,界面也会随之更新,对于一个界面,state 是 single source of truth(并不知道该怎么翻译)

    State 实例和它的值并不是等价的,获取它的值可以直接使用它的值属性

    应该通过 view 的 body 或者方法来获取 state 属性。因此,建议将 state 属性定义成私有来防止其他 view 来获取它。

    可以使用 binding 来绑定一个 state,或者使用 $ 前缀

    所以,我们仅需在 Button 的 action 中 self.isPlaying.toggle() 来更新,同时也要给 isPlaying 使用 @State 的 property wrapper

    struct ContentView: View {
    	@State private var isPlaying: Bool = false
    	
    	var body: some View {
    		Button(action: {
    			self.isPlaying.toggle()
    		}) {
    			Image(name: self.isPlaying ? "pause" : "play")
    		}
    	}
    }

    当我当前这个 View 比较复杂时,我需要拆分组件,这时候我要把 Button 作为一个独立的组件,我们通过 command 点击 Button,使用 Estract Subview 来抽取组件

    struct ContentView: View {
        @State var isPlaying: Bool = false
        
        var body: some View {
            HStack {
                PlayerButton()
            }
        }
    }
    
    struct PlayerButton: View {
        var body: some View {
            Button(action: {
                self.isPlaying.toggle()
            }) {
                Image(systemName: isPlaying ? "pause" : "play")
            }
        }
    }

    但是 PlayerButton 缺少了 isPlaying 的状态。

    @Binding

    我们可以通过 SwiftUI 的另一个 Property Wrapper 来实现组件间状态的传递。

    struct ContentView: View {
        @State var isPlaying: Bool = false
        
        var body: some View {
            HStack {
                PlayerButton(isPlaying: self.$isPlaying)
            }
        }
    }
    
    struct PlayerButton: View {
    	@Binding var isPlaying: Bool
    
        var body: some View {
            Button(action: {
                self.isPlaying.toggle()
            }) {
                Image(systemName: isPlaying ? "pause" : "play")
            }
        }
    }

    特别需要注意的地方是,我们这边需要用上 $ 符号来做传递。

    但是这样做会有一些问题 1. 如果我嵌套了好几级组件,而我最里层的组件需要用到的状态是在最顶层设置的,那么我们需要一级一级的往下传递,而中间几层又并不需要这个属性,就导致了在组件的定义中添加不必要的属性,降低了代码的可读性 2. 状态零散的散布在各个 View 中,不利于管理

    @ObservedObject

    当我们的状态比较复杂后,又或者我们需要在多个界面共享状态时,我们需要用到 @ObservedObject,这里似乎是借鉴了 Redux 的思想,我们会定义一个 Store 用来做全局的状态存储,为了能够让它可以使用 @ObservedObjet 我们需要遵循 ObservableObject 协议,然后把我们的 isPlaying 属性放到 Store 中存储

    final class Store: ObservableObject {
    	@Published var isPlaying: Bool = false
    }

    然后将 @State var isPlaying: Bool = false 替换成 @ObservedObject var store = Store() 即可

    struct ContentView: View {
        @ObservedObject var store = Store()
        
        var body: some View {
            PlayerButton(isPlaying: self.$store.isPlaying)
        }
    }

    但是这样做依然解决不了组件间需要不断传递这个 Store 的问题,我的解决方案很简单,使用单例

    final class Store: ObservableObject {
    	static let shared = Store()
    
    	@Published var isPlaying: Bool = false
    }

    这样我们在需要的地方,获取这个 Store.shared 中的属性,对其修改后也能在其他页面作出相应的修改。

    小结

    以上算是我近一个月用 SwiftUI 写 side projects 时的一些心得,主要是 @State@Binding@ObservedObject 这三个 property wrapper 在 SwiftUI 数据流中的使用

    参考资料

    1.  Managing Data Flow in SwiftUI 
    2. WWDC2019 Session 226 Data Flow Through SwiftUI
    3.  What’s the difference between @ObservedObject, @State, and @EnvironmentObject? 

    欢迎使用由 Ray Zhao 开发的产品

    番茄计

    在待办事项的基础上开启番茄钟,更好的查看您的时间管理