@State、@StateObject和@EnviornmentObject等
- @ObservedObject,@State和@EnvironmentObject
- 运用@StateObject来创立和监控外部目标
- 运用@ObservedObject从外部目标办理状况
- @EnvironmentObject来同享视图之间的数据()
- ObjectWillChange手动发送状况更新
- 常量绑定
- 自定义绑定
- Timer
- 在状况改动时运用onChange()运转一些代码
- 在明暗方式下显现不同的图画和其他视图
概述
文章首要分享SwiftUI Modifier的学习过程,将运用事例的方式进行说明。内容深入浅出,AdvancedState部分没有调试成果展示,不过测试代码是彻底的。假如想要运转成果,能够移步Github下载code -> github事例链接
1、@ObservedObject,@State和@EnvironmentObject之间的差异是什么?
在任何现代应用程序中,state
都是不可避免的,但关于SwiftUI,一切的视图都仅仅其状况的函数–不直接改动视图,而是操作状况,让状况决定成果。
运用state最简略的办法是运用@State
特点包装器
@State private var tapCount = 0
Button("Tap count: \(tapCount)") {
tapCount += 1
}
1.1、@State
在视图中创立了一个特点,可是运用@State
特点包装器来恳求SwiftUI办理内存。一切的视图都是结构体,这代表着他们不能被改动。所以,当运用@State去创立一个特点时,把对它的操控交给SWiftUI,这样只需视图存在,它就会在内存中坚持状况,当状况改动时,SwiftUI依据最新的改动从头加载视图,这样就能够更新视图了。
@State关于特点特定视图且永久不会在视图外运用的简略特点十分有用,因而将这些特点标记为私有以强化这样的主意:这种状况时专门规划的,永久不会逃离其视图。
1.2、@ObservedObject
关于更杂乱的特点–当有一个想要运用的自定义类型,或许有多个特点和办法,或许或许在多个视图之间同享–一般运用@ObservedObject
。这与@State十分相似,仅仅现在运用的是外部引用类型,而不是简略的本地特点(如字符串)。除了现在要担任办理自己的数据–需求创立类的实例,特点等等,视图会依赖于动态数据。
不管运用@ObservedObject的类型是什么,都应该恪守ObserableObject
协议。当向可调查目标增加特点时,能够决定对每个特点的更改是否应该强制监督目标的视图改写,一般会这么做,但不是必须的。
调查目标有几种办法能够告诉视图数据现已更改,但最简略的办法是运用@Published
特点包装器,假如需求更多的操控,也能够运用Combine结构中的自定义发布者,但实践上这种状况十分罕见。假如可调查目标可巧有多个殊途在运用他的数据,任何改动都会告诉一切视图。
调查目标有几种办法能够告诉视图数据现已更改,但最简略的办法是运用@Published特点包装器,假如需求更多的操控,也能够运用Combine
结构中的自定义发布者,但实践上这种状况十分罕见。假如可调查目标可巧有多个殊途在运用他的数据,任何改动都会告诉一切视图。当运用自定义发布器宣告目标已更改时,必须在主线程。
1.3、@StateObject
@StateObject
位于@State和@ObservedObject之间,这是ObservedObject的一个特别版本,原理几乎彻底相同:必须恪守ObservableObject协议,能够运用@Published将特点标记为引起更改告诉,而且任何调查@StateObject的视图都会在目标更改时改写其主体。@StateObject和@ObservedObject之间有一个重要的差异,那便是一切权–那个视图创立了目标,那个视图在调查它。
规则是这样的:不管哪个视图是第一个创立目标的,都必须运用@StateObject,告诉SwiftUI它是数据的一切者,并担任坚持数据存活。一切其他视图都必须运用@ObservedObject来告诉SwitUI他们想要调查目标的改动,但不直接具有它。
1.4、@EnvironmentObject
现已了解@State如何为一个类型声明简略的特点,当它改动时主动改写视图。以及@observedObject假如为一个外部类型声明特点,当他改动时或许会或不会导致视图改写,这两个都必须有视图设置,但@ObsrevedObject能够与其他视图同享。
还有一种特点包装器,它是@EnvironmentObject
,这是一个经过应用程序本身供给给视图的值–它是每个视图都能够读取的同享数据,假如引用有一些重要模型数据一切的视图都需求读取,能够把它从一个视图传递到另一个视图,或许把它放到每个视图都能及时拜访的环境中。
当在应用程序中传递很多数据时,把@Environment
看作一个巨大的便利结构器,由于一切的视图都指向同一个模型,假如一个视图改动了模型,一切的视图都会当即更新,规避app不同部分不同步的危险。
总结
- 关于归于单个视图的简略特点运用@State,一般将特点标记为private
- 关于或许归于多个视图的杂乱特点,运用@ObservedObject,在运用引用类型时,大多数状况下应该运用@ObservedObject
- 关于运用的每个可调查目标,不管你的代码的哪一部分担任创立它,都要运用一次@StateObject
- 关于在应用程序其他对方创立的特点,比方同享数据,运用@Environmentobject
2、运用@StateObject来创立和监控外部目标
SwiftUI的@StateObject
特点包装器是@observedObject的一种特别方式,具有相同的功用,但有一个重要的补充,由被调查目标创立,而不仅仅是存储外部传递的目标。
当用@StateObject给视图增加特点时,SwiftUI会认为这个视图是这个可调查目标的持有者,一切其他给传递目标的视图都应该运用@observedObject。
所以,假如在某个地方运用@StateObject创立了可调查目标,在你传递该目标的一切后续地方,都必须运用@ObservedObject。
class Player: ObservableObject {
@Published var name = "meta BBlv"
@Published var age = 29
}
struct FFStateObjectMonitorExternal: View {
@StateObject var player = Player()
var body: some View {
NavigationStack {
NavigationLink {
PlayerNameView(player: player)
} label: {
Text("Show Detail View")
}
}
}
//假如很难记住差异,每当在特点包装器中看到State,比方@State、@StateObject、@GestureState等,就意味着当时视图是这个数据的具有者。
}
struct PlayerNameView: View {
@ObservedObject var player: Player
var body: some View {
Text("Hello, \(player.name)")
}
}
3、运用@ObservedObject从外部目标办理状况
当运用调查目标时,需求处理三件关键作业:ObservableObject
协议与一些能够存储数据的类一同运用。@ObservedObject
特点包装器在视图中用于存储可调查目标实例,@Published
特点包装器被增加到调查目标中的任何特点,当视图产生改动时,这些特点会导致视图改写。
关于从其他地方传入的视图,只运用@ObservedObject是十分重要的,你不应该运用这个特点包装器来创立一个可调查目标的初始实例–这便是@StateObject的作用。
class UserProgress: ObservableObject {
@Published var score = 0
}
struct InnerView: View {
@ObservedObject var progress: UserProgress
var body: some View {
Button("Increase Score") {
progress.score += 1
}
}
}
struct FFObservedObjectManageState: View {
@StateObject var progress = UserProgress()
var body: some View {
//ObservableObject的一致性答应在视图中运用这个累的实例,这样当产生改动时,视图就会从头加载。
//@Published特点包装器告诉SwiftUI,对score的更改应触发视图重载。
VStack {
Text("Your score is \(progress.score)")
InnerView(progress: progress)
}
//除了在progress中运用@ObservedObject特点包装器之外,其他的一切看起来都差不多--SwiftUI为咱们处理了一切的细节。
//可是,有一个重要的差异,progress没有声明为私有,这是由于绑定目标能够被多个视图运用,因而公开同享它时很常见的。
//请不要运用@ObservedObject来创立目标的实例,假如想要创立实例,运用@StateObject。
}
}
4、@EnvironmentObject来同享视图之间的数据
关于应该与应用程序中的许多视图同享数据,SwiftUI供给了@EnvironmentObject
特点包装器,这能够在任何需求的地方同享模型数据,一同还保证当数据产生改动时,视图主动坚持更新。把@EnvironmentObject看作是在许多视图上运用@ObservedObject的一种更智能更简略的方式。在视图A中创立数据,然后将其传递给视图B,然后传递给视图C,再传递给视图D,不如在视图A中穿件它并将其放入环境中,以便视图B、C和D将主动拜访它。
就像@ObservedObject相同,你永久不会给@EnvironmentObject特点赋值。相反,它应该在其他地方传入,最终或许在某处运用@StateObject来创立它。可是,与@ObservedObject不同,不需求手动将目标传递给其他视图,相反,运用send数据到一个叫environmentObject()
修饰符中,这使得该目标在SwiftUI的环境中对该视图以及其内部的任何其他视图可用。
环境目标必须有根视图供给,假如SwiftUI找不到正确类型的环境目标,就会crash。
class GameSettings: ObservableObject {
@Published var score = 0
}
struct ScoreView: View {
@EnvironmentObject var settings: GameSettings
var body: some View {
Text("Score: \(settings.score)")
}
}
struct FFEnvironmentShare: View {
@StateObject var settings = GameSettings()
var body: some View {
NavigationStack {
VStack {
Button("Increase Score") {
settings.score += 1
}
NavigationLink {
ScoreView()
} label: {
Text("Show Detail View")
}
}
.frame(height: 200)
}
.environmentObject(settings)
}
//这段代码中有一些重要的内容:
//就像@StateObject与@ObservedObject相同,与@EnvironmentObject一同运用的一切类都必须恪守ObservableObject协议。
//将GameesSettings放入导航Stack环境中,这意味着navigationStack中一切的视图都能够读取该目标,以及navigationStack显现的任何视图。
//当运用@EnvironmentObject特点包装器是,声明晰期望承受的目标类型,而不是创立它--究竟,期望在环境中获取它。
//由于Detail视图显现在NavigationStack中,它将拜访相同的环境,这反过来意味着它能够读取创立的gamesSetting目标。
//不需求显现的将环境中的gamesettings实例与scoreView的settings特点关联起来--SwiftUI会主动核算它在环境中有一个gamesSetting实例,所以那便是它运用的。
//已然视图依赖于当时的环境目标,那么更新与来代码以供给一些示例设置是很重要的。例如,运用ScoreView().environmentObject(gamesetting())之类的预览应该能够做到这一点。
//假如需求向环境中增加多个目标,则应该增加多个environmentObject()修饰符--只需一个接一个调用。
}
5、ObjectWillChange手动发送状况更新
尽管运用@published
是操控状况更新最简略的办法,但假如需求某些特定的东西,也能够手动操作,例如,当你对给定值符合条件才改写视图。一切可调查目标会主动拜访ObjectWillChange
特点时,该特点本身有一个send()办法,能够在想要改写调查视图时调用他。
class UserAuthentication: ObservableObject {
var username = "meta BBLv" {
willSet {
objectWillChange.send()
}
}
}
struct FFObjectWillChange: View {
@StateObject var user = UserAuthentication()
var body: some View {
VStack(alignment: .leading) {
TextField("Enter your name", text: $user.username)
Text("Your username is: \(user.username)")
}
}
//如何将willSet特点调查者附加到UserAuthencation的username特点上的,在该值产生改动时运转代码。在实例代码中,只需username产生改动时,就调用objectWillChange.send(),这将告诉objectWillChange发布者发布数据产生改动的音讯。以便任何订阅的视图都能够改写。
//这个示例在特点上运用@Published没有什么不同,可是现在又了对objectWillChange.send()的自定义调用,能够增加额定的功用,例如,将值保存到磁盘上。
}
6、常量绑定
当制造一些UI时,或许只需求传递一个值给SwiftUI与来一些有意义的东西来展示时,运用常量绑定
很有帮助,硬编码的值不会改动,但仍然能够向惯例绑定相同运用。
例如,假如想创立一个切换开关,一般需求创立一个@State特点来保存bool值,然后在创立时将其发送到切换开关中,可是,假如仅仅在原型化界面,能够运用常量绑定
struct FFConstantBindings: View {
var body: some View {
Toggle(isOn: .constant(true), label: {
Text("Show advanced options")
})
//这个开关是只读的,而且总是翻开的,由于这便是运用了常量绑定,在后面接入实践数据时运用@State特点来替换他。
//这些常量绑定有各种类型,bool、string、int等,SwiftUI会保证为每种视图类型运用正确的绑定。
}
}
7、自定义绑定
当运用SwiftUI的@State特点包装器时,它代表咱们做了很多的作业来答应用户界面控件的双向绑定。可是,咱们也能够运用Binding
类型手动创立绑定,该类型能够供给自定义的get
和set
闭包,以便在读取和写入时运转。
struct FFCustomBindings: View {
@State private var username = ""
@State private var firstToggle = false
@State private var secondToggle = false
var body: some View {
let binding = Binding {
self.username
} set: {
self.username = $0
}
VStack {
TextField("Enter your name", text: binding)
}
//当绑定到自定义binding实例时,你不需求在绑定称号前运用$符号,由于你现已读取了双向绑定。
//当你期望为正在读取或写入的绑定增加额定的逻辑时,自定义绑定十分有用,你或许期望在发送值返回之前履行一些核算,或许你或许期望在值更改时采纳一些额定的操作。
//例如,创立两个toggle的stack,其间两个开关封闭,其间一个能够翻开,但两个都不能一同翻开,启动其间一个将一直禁用别的一个。
let firstBinding = Binding {
self.firstToggle
} set: {
self.firstToggle = $0
if $0 == true {
self.secondToggle = false
}
}
let secondBinding = Binding {
self.secondToggle
} set: {
self.secondToggle = $0
if $0 == true {
self.firstToggle = false
}
}
VStack {
Toggle(isOn: firstBinding, label: {
Text("First Toggle")
})
Toggle(isOn: secondBinding, label: {
Text("Second Toggle")
})
}
}
}
8、Timer
假如想要定期运转一些代码,或许需求制造一个倒计时计时器,应该运用timer
和onReceive()
修饰符
struct FFTimer: View {
@State var currentDate = Date.now
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State var timeRemaining = 10
let timer1 = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer, perform: { input in
currentDate = input
})
//关于Runloop选项运用.main很重要,由于计时器将更新用户界面,至于.common方式,它答应计时器与其他常见事件一同运转,例如,文本在视图中翻滚。
//onReceive()闭包被传入一些包含当时日期的输入。在上面的代码中,将其直接赋值给currentDate,可是你能够运用它来核算从上一个日期到现在现已过去了多少时刻。
//假如你特别期望创立一个倒计时器或许秒表,则应该创立一些状况来盯梢剩余的时刻,然后在计时器触发时减去剩余时刻。
//创立倒计时器,在label上显现剩余时刻。
Text("倒计时: \(timeRemaining)")
.onReceive(timer1) { input in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}
9、在状况改动时运用onChange()运转一些代码
SwiftUI能够使onChange()
修饰符附加到任何视图上,当程序中的某些状况产生改动时,它将运转你想要运转的代码,由于咱们不能总是把特点调查者如didSet
与@State
一同运用。
extension Binding {
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
Binding {
self.wrappedValue
} set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
}
}
struct FFStateOnchange: View {
@State private var name = ""
@State private var name1 = ""
@State private var name2 = ""
@State private var name3 = ""
var body: some View {
//此函数在ios17上现已改动
TextField("Enter your name:", text: $name)
.textFieldStyle(.roundedBorder)
.onChange(of: name) { newValue in
print("Name changed to \(name)!")
}
//假如OS在iOS17以及以后,有一个不承受参数的声明,能够直接读取特点并保证获得它的新值。
//iOS17还供给了两外两个函数,一个承受带参数的两个闭包,一个用于旧值,一个用于新值,另一个用于确定视图第一次显现时是否应该运转action函数。
//例如:当产生改动时,打印旧值和新值。
TextField("Enter your name", text: $name1)
.onChange(of: name1) { oldValue, newValue in
print("Change from \(oldValue) to \(newValue)")
}
//当值改动时打印一条简略的音讯,可是经过initial:true也会在显现视图时触发action闭包。
TextField("Enter your name", text: $name2)
.onChange(of: name2, initial: true) {
print("Name is now \(name2)")
}
//运用initial:true是一种十分有用的整合功用的办法--而不是在onAppear()和onChange()中做一些作业,你能够一次完成一切的作业。
//你或许更喜欢想Binding增加一个自定义扩展,这样我就能够将调查代码直接附加到绑定而不是视图上--它答应我讲调查者放在它正在调查的事物旁边,而不是在视图的其他地方附加许多onChange修饰符。
TextField("Enter your name:", text: $name3.onChange(nameChanged(to:)))
}
//也便是说,假如这样做,请保证经过工具运转你的代码--在视图上运用onChange()将它增加到绑定中性能更高。
func nameChanged(to value: String) {
print("Name changed to \(name3)!")
}
}
10、在明暗方式下显现不同的图画和其他视图
SwiftUI能够依据用户当时的外观设置直接从你的ASset catalog
中加载明暗方式
的图画,但假如不运用Asset catalog,例如,假如你下载图画或在本地生成他们。最简略的解决方案是创立一个一同处理明暗方式图画的新视图
struct AdaptiveImage: View {
@Environment(\.colorScheme) var colorScheme
let light: Image
let dark: Image
@ViewBuilder var body: some View {
if colorScheme == .light {
light
} else {
dark
}
}
}
//它保留了相同的便捷初始化器,但现在增加了承受闭包的代替办法。所以,现在能够利用闭包在明暗之下切换更杂乱的代码
struct AdaptiveView<T: View, U: View>: View {
@Environment(\.colorScheme) var colorScheme
let light: T
let dark: U
init(light: T, dark: U) {
self.light = light
self.dark = dark
}
init(light: () -> T, dark: () -> U) {
self.light = light()
self.dark = dark()
}
@ViewBuilder var body: some View {
if colorScheme == .light {
light
} else {
dark
}
}
}
struct FFDarkMode: View {
var body: some View {
//这样能够传入两张图,SwiftUI会主动挑选正确的明暗方式。
AdaptiveImage(light: Image(systemName: "sun.max"), dark: Image(systemName: "moon"))
//假如你仅仅想在明暗方式的之间切换,这很有用,但假如想要增加一些额定的代码,咱们能够创立一个包装器视图,能够依据明暗方式显现彻底不同的内容。
VStack {
AdaptiveView {
VStack {
Text("Light mode")
Image(systemName: "sun.max")
}
} dark: {
HStack {
Text("Dark mode")
Image(systemName: "moon")
}
}
.font(.largeTitle)
}
}
}