SwiftUI基础篇AdvancedState

@State、@StateObject和@EnviornmentObject等

概述

文章主要分享SwiftUI Modifier的学习过程,将使用案例的方式进行说明。内容浅显易懂,AdvancedState部分没有调试结果展示,不过测试代码是齐全的。如果想要运行结果,可以移步Github下载code -> github案例链接

1、@ObservedObject,@State和@EnvironmentObject之间的区别是什么?

在任何现代应用程序中,state都是不可避免的,但对于SwiftUI,所有的视图都只是其状态的函数--不直接改变视图,而是操纵状态,让状态决定结果。

使用state最简单的方法是使用@State属性包装器

Swift 复制代码
@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。

Swift 复制代码
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的作用。

Swift 复制代码
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。

Swift 复制代码
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()方法,可以在想要刷新观察视图时调用他。

Swift 复制代码
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值,然后在创建时将其发送到切换开关中,然而,如果只是在原型化界面,可以使用常量绑定

Swift 复制代码
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闭包,以便在读取和写入时运行。

Swift 复制代码
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()修饰符

Swift 复制代码
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一起使用。

Swift 复制代码
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,例如,如果你下载图像或在本地生成他们。最简单的解决方案是创建一个同时处理明暗模式图像的新视图

Swift 复制代码
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)
        }
    }
}
相关推荐
今天也想MK代码2 天前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
東三城8 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节
今天也想MK代码10 天前
基于swiftui 实现3D loading 动画效果
ios·swiftui·swift
胖虎111 天前
SwiftUI(五)- ForEach循环创建视图&尺寸类&安全区域
ios·swiftui·swift·foreach·安全区域
RamboPan12 天前
Mac 使用脚本批量导入 Apple 歌曲
macos·自动化·shell·apple·script
zyosasa17 天前
SwiftUI 精通之路 11: 栅格布局
前端·swiftui·swift
小溪彼岸21 天前
【iOS小组件实战】灵动岛实时进度通知
swiftui·swift
提笔忘字的帝国25 天前
【ios】SwiftUI 混用 UIKit 的 Bug 解决:UITableView 无法滚动到底部
swiftui·bug·xcode
zyosasa25 天前
SwiftUI 精通之路 09:ForEach 视图构造器的基础应用
swiftui·swift
杂雾无尘25 天前
Swift 6 正式发布:从应用到嵌入式,Swift 的新征程!
ios·swift·apple