@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类型手动创立绑定,该类型能够供给自定义的getset闭包,以便在读取和写入时运转。

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

假如想要定期运转一些代码,或许需求制造一个倒计时计时器,应该运用timeronReceive()修饰符

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)
        }
    }
}