iOS 入门系列(三)

1,591 阅读7分钟

引言

在本篇文章之前的 iOS 入门系列(一)iOS 入门系列(二)已经分别介绍了iOS 开发开发语言开发工具以及开发框架等内容;且所有内容均收集在 iOS 入门系列合集 中。在本篇将会对SwiftUI 框架中的数据状态和数据流进行介绍

数据状态和数据流开启我们入坑之旅的第二站吧~

State And Data Flow(数据状态和数据流)

SwiftUI 框架中是通过属性包装器来管理数据的状态流动传递的。数据状态和数据流也是掌握并进行iOS 开发需要了解的重点知识~

下面便是SwiftUI中的属性包装器,接下来会一一对其进行介绍:

  • @State
  • @Published
  • @StateObject
  • @ObservedObject
  • @EnvironmentObject
  • @Environment()
  • @binding

@State

用于在视图内部管理和维护短暂的、局部的状态

@State属性包装器用于在视图内部管理和存储单一数据值,它通常用于表示视图的内部状态或用户界面的临时状态

import SwiftUI

struct CounterView: View {
    @State private var count: Int = 0
}

@State直接在视图中声明变量后即可在视图内部管理和响应数据的变化,从而实现视图的动态更新,使用较为频繁

@Published

用于在ObservableObject 类型中标记属性,使其成为可被观察的属性

@Published属性包装器通常用于在ObservableObject类中声明被发布的属性,以便在属性值发生变化时通知订阅者

import SwiftUI

class User: ObservableObject {
    @Published var username: String = "Guest"
}

直接在ObservableObject 类型的对象中声明被发布的属性,然后通过@StateObject@ObservedObject@EnvironmentObject来实现订阅这些属性

@StateObject

用于在视图中创建并维护一个持久化的、全局的ObservableObject 实例

@StateObject适用于在视图内部创建和管理对象,它会自动在视图销毁时销毁对象。这通常用于ViewModel,因为ViewModel 对象的生命周期通常与视图相同

下面是使用@StateObject的案例:

class ViewModel: ObservableObject {
    @Published var data: String = "Initial Data"
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        Text(viewModel.data)
            .onTapGesture {
                viewModel.data = "Updated Data"
            }
    }
}

可以看出,在view 视图中通过@StateObject声明的属性是在视图中自己创建的

@ObservedObject

用于在视图中引用一个外部创建的ObservableObject 实例

@ObservedObject适用于从外部传递对象给视图,并且希望视图能够观察该对象的变化。对象的生命周期不由视图管理,而是由传递者管理。适用于外部创建了一个对象并希望多个视图都能观察和使用它

class ViewModel: ObservableObject {
    @Published var data: String = "Initial Data"
}

struct ContentView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Text(viewModel.data)
            .onTapGesture {
                viewModel.data = "Updated Data"
            }
    }
}

struct ParentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        NavigationView {
            NavigationLink(destination: ContentView(viewModel: viewModel)) {
                Text("Go to ContentView")
            }
        }
    }
}

可以看出,在view 视图中通过@ObservedObject声明的属性只是在视图中进行了声明,需要通过父视图将对象传递给子视图

@EnvironmentObject

用于在整个应用程序中共享一个ObservableObject 实例

@EnvironmentObject适用于将对象在整个视图层次结构中自动传递给所有需要的视图。需要在应用程序的根视图中将一个自定义的数据对象设置为环境对象。然后多个视图就能够共享相同的数据,并在数据变化时进行自动更新

根视图设置环境对象

class UserModel: ObservableObject {
    @Published var username: String = ""
    @Published var age: Int = 0
}

@main
struct YourApp: App {
    @StateObject private var userModel = UserModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(userModel)
        }
    }
}

@EnvironmentObject 声明对象

struct ContentView: View {
    @EnvironmentObject var userModel: UserModel

    var body: some View {
        VStack {
            Text("Username: \(userModel.username)")
            Text("Age: \(userModel.age)")
        }
    }
}

@Environment()

用于在视图中访问系统提供的环境变量

@Environment属性包装器用于访问环境变量,这些变量由系统提供或在应用程序范围内设置

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        if colorScheme == .dark {
            Text("Dark Mode")
                .foregroundColor(.white)
                .background(Color.black)
        } else {
            Text("Light Mode")
                .foregroundColor(.black)
                .background(Color.white)
        }
    }
}

通过系统提供的.colorScheme变量来访问当前的颜色方案,@Environment()在访问系统变量时需要带上\

@binding

用于在视图之间双向绑定属性,实现数据传递和同步

@Binding属性包装器用于在父视图中创建一个绑定,然后将其传递给子视图,以便子视图可以修改父视图中的数据。这对于在视图层次结构中共享数据非常有用

import SwiftUI

struct ContentView: View {
    @State private var isToggleOn = false
    
    var body: some View {
        ToggleView(isOn: $isToggleOn)
            .padding()
    }
}

struct ToggleView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Toggle("Toggle", isOn: $isOn)
    }
}

父视图中创建一个属性并将其标记为@State或其他可修改的属性包装器,然后将这个属性传递给子视图中用@Binding属性包装器声明的绑定属性。子视图可以使用这个绑定属性读取修改父视图中的属性值

拓展

  1. 在SwiftUI中,还有其他带有@符号的属性和特性,但它们却不是属性包装器。下面列举我目前使用到的:

    • 视图构建:@ViewBuilder

      可以将多个视图作为闭包参数传递给函数,然后在函数中使用这些视图构建UI 层次

    • 函数特性:@escaping

      @escaping 主要用于函数参数的声明,用于函数类型参数的标记,用来指示该闭包参数逃逸闭包,会在函数执行结束后继续存在并被调用

  2. 为什么要存在这么多不同层级的传递数据的属性包装器?

    这里拿@ObservedObject@EnvironmentObject对比说明,@EnvironmentObject既然能在全局共享数据,那为什么还需要@ObservedObject或者其他属性包装器

    虽然@EnvironmentObject在全局数据共享方面非常强大,但并不是所有情况都适合使用它,而 @ObservedObject在某些场景下仍然有其优势和用武之地:

    1. 局部性@ObservedObject更适用于视图的局部性数据共享。如果数据只在某个特定的视图层次结构内使用,将其作为@ObservedObject传递给子视图会更清晰和合适
    2. 粒度控制@ObservedObject允许您在不同层次的视图中使用不同的数据对象,每个视图可以有自己的数据源,而@EnvironmentObject会在整个应用程序中共享相同的数据对象
    3. 性能消耗:由于@EnvironmentObject是全局共享的,当应用程序中的多个视图都依赖于同一个@EnvironmentObject时,可能会导致性能问题。每次数据更改时,所有依赖于该对象的视图都会重新计算,这可能会导致不必要的性能消耗
    4. 测试和解耦:使用@ObservedObject可以更容易进行单元测试,因为您可以更精确地控制要传递的数据对象。此外,使用@ObservedObject可以更好地解耦视图,使其更加独立和可复用

    综上所述,@EnvironmentObject在全局数据共享方面是非常强大的,但在一些特定场景下,使用 @ObservedObject可能更加合适,以达到更好的性能、测试和解耦效果

小结

本文主要是针对SwiftUI属性包装器对于视图及视图间数据状态和数据流的管理,下面就对全文做一个简要总结

@State@Published使用及理解起来相对容易,一个是在视图内声明单一数据值,一个是在ObservableObject 类型对象中声明被发布的属性值

@StateObject@ObservedObject在使用时可能会存在一些困惑,但只要牢记@StateObject是在视图内创建ObservableObject 实例,而@ObservedObject则是在视图内引用ObservableObject 实例即可

@ObservedObject@EnvironmentObject在理解上也会存在一些疑惑,但只要明白:@ObservedObject需要通过父视图将对象传递给子视图,而@EnvironmentObject只需要在根视图中传递一次,之后所有视图都能共享这个对象;@ObservedObject更适用于局部范围的数据共享,而@EnvironmentObject更适用于全局范围的数据共享的概念后也能正确使用它们了

@EnvironmentObject@Environment()虽然长得相似,但是也是不同的,@EnvironmentObject用于在视图之间共享自定义数据对象,而@Environment用于访问系统提供的环境变量

@binding也同@State@Published@StateObject@ObservedObject@EnvironmentObject@Environment()等属性包装器一样这些与数据状态、流动和传递相关,能实现在视图之间创建双向绑定