普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月26日首页

Swift UI 状态管理

作者 Haha_bj
2025年11月26日 17:56

一、@State State修饰的属性是值传递

SwiftUI管理声明为state的存储属性。当值发生变化时,SwiftUI会更新视图层次结构中依赖于该值的部分。对这个属性进行赋值的操作将会触发 View 的刷新,它的 body 会被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新的 View,并进行刷新。

struct JLStateView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Text("\(count)")
            Button("按钮点击加1") {
                count += 1
            }
            .background(.orange)
            
        }
    }
}

通过@State定义变量count,点击按钮会触发Text中数字的显示

  • 不要在视图层次结构中实例化视图的位置初始化视图的状态属性,因为这可能与SwiftUI提供的存储管理冲突。

  • 为了避免这种情况,总是将state声明为private,并将其放在视图层次结构中需要访问该值的最高视图中。

@State private var count = 0

二、@Binding

@State修饰的属性是值传递,因此在父视图和子视图之间传递属性时。子视图针对属性的修改无法传递到父视图上。

Binding修饰后会将属性会变为一个引用类型,视图之间的传递从值传递变为了引用传递,将父视图和子视图的属性关联起来。这样子视图针对属性的修改,会传递到父视图上。

需要在属性名称前加上一个美元符号$来获得这个值。

被声明为 @Binding 的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递.

import SwiftUI
struct JLBtnView: View {
    @Binding var isShowText: Bool
    var body: some View {
        Button("按钮点击") {
            isShowText.toggle()
        }
    }
}

struct JLContentView: View {
    @State private var isShowText: Bool = true
    var body: some View {
        VStack {
            if isShowText{
                Text("点击后会被隐藏")
            }else{
                Text("点击后会被显示")
            }
            /// $isShowText 双向绑定
            JLBtnView(isShowText: $isShowText)
        }
    }
}
  • 按钮在JLBtnView视图中,并且通过点击,修改isShowText的值。

  • 将jLBtnView视图添加到JLContentView上作为它的子视图。并且传入isShowText。

  • 此时的传值是指针传递,会将点击后的属性值传递到父视图上。

  • 父视图拿到后也作用在自己的属性,因此他的文本视图会依据该属性而隐藏或显示

  • 如果将@Binding改为@State,会发现点击后不起作用。这是因为值传递子视图的更改不会反映到父视图上

struct JLContentView: View {
    @State private var name: String = ""
    var body: some View {
        VStack {
            TextField("请输入您的名字",text: $name)
            Text(name)
            
        }
    }
}
  • 在文本输入框中输入的数据,就会传入到name中

  • 同时name又绑定在文本视图上,所以会将文本输入框输入的文本显示到文本视图上

  • 这就是数据绑定的快捷实现。

三、@ObservedObject

如果说 @State 是全自动驾驶的话,ObservableObject 就是半自动,它需要一些额外的声明。ObservableObject 协议要求实现类型是 class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行“广播”,它的订阅者 (一般是 View 相关的逻辑) 在收到通知后,对 View 进行刷新。
创建 ObservableObject 后,实际在 View 里使用时,我们需要将它声明为 @ObservedObject。这也是一个属性包装,它负责通过订阅 objectWillChange 这个“广播”,将具体管理数据的 ObservableObject 和当前的 View 关联起来。

  • 绑定的数据是一个对象。

  • 被修饰的对象,其类必须遵守ObservableObject协议

  • 此时这个类中被@Published修饰的属性都会被绑定

  • 使用@ObservedObject修饰这个对象,绑定这个对象。

  • 被@Published修饰的属性发生改变时,SwiftUI就会进行更新。

import SwiftUI
internal import Combine

class Persion: ObservableObject{
    /// 属性只有被@Published修饰时,属性的值修改时,才能被监听到
    @Published var name = ""
}

struct JLContentView: View {
    @ObservedObject var p = Persion()
    var body: some View {
        VStack {
            Text(p.name)
                .padding()
            Button("修改") {
                p.name = "哈哈"
            }
            
        }
    }
}

@ObservedObject修饰的必须是遵守ObservableObject 协议的class对象
class对象的属性只有被@Published修饰时,属性的值修改时,才能被监听到

四、@EnvironmentObject

在多视图中,为了避免数据的无效传递,可以直接将数据放到环境中,供多个视图进行使用

在 SwiftUI 中,View 提供了 environmentObject( 方法,来把某个 ObservableObject 的值注入到当前 View 层级及其子层级中去。在这个 View 的子层级中,可以使用 @EnvironmentObject 来直接获取这个绑定的环境值。

extension View {

    @inlinable nonisolated public func environmentObject<T>(_ object: T) -> some View where T : ObservableObject
}

final class Persion: ObservableObject{
    @Published var name = "哈哈"
}
struct MapView: View {
    @EnvironmentObject var p : Persion
    var body: some View {
        VStack {
            Text(p.name)
            Button("点击") {
                p.name = "呵呵"
            }
        }
    }
}

struct JLContentView: View {
    
    var body: some View {
        VStack {
            let p = Persion()
            MapView().environmentObject(p)
        }
    }
}

@EnvironmentObject 修饰器是针对全局环境的。通过它,我们可以避免在初始 View 时创建 ObservableObject, 而是从环境中获取 ObservableObject
可以看出我们获取 p这个 ObservableObject 是通过 @EnvironmentObject 修饰器,但是在入口需要传入 .environmentObject(p) 。@EnvironmentObject 的工作方式是在 Environment 查找 Person 实例。

import SwiftUI
internal import Combine

final class Persion: ObservableObject{
    @Published var name = "哈哈"
}

struct EnvView: View {
    @EnvironmentObject var p : Persion
    var body: some View {
        Text(p.name)
    }
}

struct BtnView: View {
    @EnvironmentObject var p: Persion
    var body: some View {
        Text(p.name)
        Button("修改") {
            p.name = "1123"
        }
    }
}


struct JLContentView: View {
    let p = Persion()
    var body: some View {
        VStack {
            EnvView().environmentObject(p)
            BtnView().environmentObject(p)
        }
    }
}
  • 给属性添加@EnvironmentObject修改,就将其放到了环境中。

  • 其他视图中想要获取该属性,可以通过.environmentObject从环境中获取。

  • 可以看到分别将EnvView和BtnvView的属性分别放到了环境中

  • 之后我们ContentView视图中获取数据时,可以直接通过环境获取。

  • 不需要将数据传递到ContentView,而是直接通过环境获取,这样避免了无效的数据传递,更加高效

  • 如果是在多层级视图之间进行传递,会有更明显的效果。

import SwiftUI
internal import Combine

final class Persion: ObservableObject{
    @Published var name = 1
    deinit{
        print("被销毁了")
    }
}

struct MapView: View {
    @ObservedObject var p = Persion()
    var body: some View {
        VStack{
            Text("\(p.name)")
            Button("+1") { //添加一个按钮,指定标题文字为 First button
                p.name += 1
            }
        
        }
    }
}

struct JLContentView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Text("刷新:\(count)")
            Button("刷新"){
                count += 1
            }
            
            MapView()

        }
    }
}

点击刷新时,Person 的deinit方法被调用,说明p对象被销毁;
先连续点击+1,Text上的数字在一直递增,当点击刷新时Text上的数字恢复为1,这个现象也说明p对象被销毁

import SwiftUI
internal import Combine


final class Persion: ObservableObject{
    @Published var name = 1
    deinit{
        print("被销毁了")
    }
}

struct MapView: View {
    
    @StateObject var p = Persion()
    var body: some View {
        VStack{
            Text("\(p.name)")
            Button("+1") { //添加一个按钮,指定标题文字为 First button
                p.name += 1
            }
        
        }
    }
}

struct JLContentView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Text("刷新:\(count)")
            Button("刷新"){
                count += 1
            }
            
            MapView()

        }
    }
}

和例1不同的是怎么操作,p都不会销毁

@StateObject的声明周期与当前所在View生命周期保持一致,即当View被销毁后,StateObject的数据销毁,当View被刷新时,StateObject的数据会保持;而ObservedObject不被View持有,生命周期不一定与View一致,即数据可能被保持或者销毁;

Swift UI数据存储

作者 Haha_bj
2025年11月26日 11:34

一. @StateObject 数据存储机制

@StateObject 保存的数据存储在设备内存(RAM)中,是临时存储

import SwiftUI
internal import Combine
class BloodGlucoseStore: ObservableObject{
    @Published var count = 0 // 存储在内存中
    
}

struct JLHomeView: View {
    @StateObject private var store = BloodGlucoseStore()// 对象存在于内存中
    var body: some View {
        Text("记录数量:\(store.count)")
        Button("点击") {
            store.count += 1
        }
        
    }
}

数据生命周期

  • 创建时机:视图第一次被创建时
  • 保持时机:视图重新渲染时数据保持不变
  • 销毁时机:视图被销毁时数据丢失
struct ContentView: View {
    @State private var showHomeView = false
    
    var body: some View {
        VStack {
            Button("显示/隐藏 HomeView") {
                showHomeView.toggle()
            }
            
            if showHomeView {
                JLHomeView()  // 创建时:数据在内存中创建
            }              // 销毁时:数据从内存中清除
        }
    }
}

二. UserDefaults 存储机制

存储位置

  • 📁 应用沙盒中的 .plist 文件
  • 路径:/Library/Preferences/[Bundle-ID].plist

UserDefaults 数据安全性

✅ 不会丢失的情况
  • 应用更新:数据保持不变
  • 应用重启:数据依然存在
  • 设备重启:数据保持不变
  • iOS 系统更新:数据通常保持
❌ 会丢失的情况
  • 卸载应用:整个应用沙盒被删除
  • 恢复设备但不恢复备份:数据丢失
  • 手动清除应用数据:通过系统设置清除
class SettingStore: ObservableObject{
    
    @Published var isDarmMode: Bool{
        didSet{
            /// 保存数据
            UserDefaults.standard.set(isDarmMode, forKey: "isDarmMode")
            UserDefaults.standard.synchronize()
        }
    }
    
    init(){
        /// 读数数据
        isDarmMode = UserDefaults.standard.bool(forKey: "isDarmMode")
    }
    deinit{
        /// 删除数据
        UserDefaults.standard.removeObject(forKey: "isDarmMode")
        UserDefaults.standard.synchronize()
    }
}

三. @Published 属性包装器

核心作用

@Published 的主要作用是自动触发 UI 更新

class CounterStore: ObservableObject {
    var count = 0  // 普通属性
    
    func increment() {
        count += 1  // UI 不会更新!
    }
}

实际应用示例

class BloodGlucoseStore: ObservableObject {
    @Published var records: [BloodGlucoseRecord] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    @Published var selectedDate = Date()
    @Published var filterType: FilterType = .all
    
    // 计算属性也会响应 @Published 属性的变化
    var filteredRecords: [BloodGlucoseRecord] {
        switch filterType {
        case .all:
            return records
        case .today:
            return records.filter { Calendar.current.isDateInToday($0.date) }
        case .thisWeek:
            return records.filter { $0.date.isInCurrentWeek }
        }
    }
    
    func addRecord(_ record: BloodGlucoseRecord) {
        records.append(record)  // 触发 UI 更新
    }
    
    func setFilter(_ filter: FilterType) {
        filterType = filter  // 触发筛选更新
    }
}

高级用法

自定义 setter
class UserStore: ObservableObject {
    @Published var username: String = "" {
        didSet {
            validateUsername()
            saveToUserDefaults()
        }
    }
    
    @Published var isUsernameValid = false
    
    private func validateUsername() {
        isUsernameValid = username.count >= 3
    }
}

级联更新
class ShoppingCartStore: ObservableObject {
    @Published var items: [CartItem] = [] {
        didSet {
            updateTotalPrice()  // items 变化时自动更新总价
        }
    }
    
    @Published var totalPrice: Double = 0
    @Published var discountCode: String = "" {
        didSet {
            updateTotalPrice()  // 折扣码变化时也更新总价
        }
    }
    
    private func updateTotalPrice() {
        let subtotal = items.reduce(0) { $0 + $1.price * Double($1.quantity) }
        let discount = calculateDiscount(for: discountCode)
        totalPrice = subtotal - discount
    }
}

❌
❌