SwiftUI 精通之路 13: @State 状态与 @Binding 绑定

725 阅读5分钟

前言

CleanShot 2024-10-28 at 17.29.39@2x.png

星光不负赶路人,时光不负有心人

正文

进入正文,我们第一步还是首先创建我们的工程文件,我们打开我们创建的项目添加新的文件。Xcode 顶部的 File > New > File form Template... 快捷打开创建模板面板的的快捷键是 ⌘ + N.

我们创建名称为 StateAndBindingBootcamp.swift 的文件, 本章节我们学习 SwiftUI 中 局部状态的管理@State以及 状态的传递绑定@Binding.

CleanShot 2024-10-28 at 20.31.00@2x.png

在开始本章案例之前我用通俗意我懂的方式说下这两个属性包装器关系。

在生活中,目前大部分手机或者电视都是可以互联,那么调节声音可以直接电视上通过物理按键调节或者通过通过遥控器,或者通过蓝牙连接手机,手机上进行操作!

其实这之间,电视音量的大小其实是电视本身的状态,通过手机或者遥控器控制音量其实间接的控制电视的音量状态。

这其实就是@State@Binding 的关系:

  1. @State 其实就类似电视上物理按键,它可以直接控制电视音量的状态。
  2. @Binding 就像遥控器上的音量控制按钮 或者手机蓝牙音量控制,它和电视共享音量状态,所以调整遥控器上的音量按钮会同步影响电视上的音量显示。

好的·那么我们进入正题:本章我们将通过这个电视以及遥控器的比喻去创建我们案例!

通过刚才的举例,我们首先创建我们的电视视图,它有一个音量的私有状态是吧,所以我们首先创建音量状态,我们通过 @State 创建视图的私有状态!

@State private var volume: Int = 10  // 音量状态,独立在电视内
CleanShot 2024-10-28 at 21.46.23@2x.png

private 表示当前状态仅当前视图以及其子视图可以使用,状态私有,增强代码可读性,后续会讲解。

状态定义好了,我们开始创建我们电视的视图,我们相对简单表示电视音量:

VStack {
    Text("当前电视音量🔊:\(volume)")
        .foregroundStyle(Color.white)
        .padding(.bottom, 10)
        .background(alignment: .bottom) {
            RoundedRectangle(cornerRadius: 10)
                .frame(width: 250, height: 200, alignment: .center)
        }
}
CleanShot 2024-10-28 at 21.56.33@2x.png

创建当前电视视图,我们使用 .background 创建圆角矩形 RoundedRectangle 的背景图层,有些简陋,但是意思到位!

电视本身肯定有音量调节按钮,我们创建两个音量调节按钮!

HStack {
    Group {
        // 音量 +
        Button {
            volume += 1
        } label: {
            Image(systemName: "speaker.plus.fill")
        }
        
        // 音量 -
        Button {
            volume -= 1
        } label: {
            Image(systemName: "speaker.minus.fill")
        }
    }
    .buttonStyle(.borderless)
    .padding(10)
    .background(Circle().fill(Color.blue))
    .foregroundStyle(Color.white)
}
CleanShot 2024-10-28 at 22.07.31@2x.png

Group 是视图类型,类似于 View 等,它本身并没有什么显示的效果,就类似于空的视图,我们可以通过它进行包裹组件视图统一进行设置一些需要重复设置的修饰器。

我们现在已经制作好了我们电视自身的音量控制按键,那么我们现在开始制作我们的遥控器视图!

在创建遥控器的时候,我们用到前面我们创建的电视视图的遥控视图

VStack {
        Text("遥控器")
            .font(.title3)
            .fontWeight(.semibold)
        HStack {
            // 使用 Group 包裹给包裹元素统一运用修饰符
            Group {
                Button {
                    volume += 1
                } label: {
                    Image(systemName: "speaker.plus.fill")
                }
                Button {
                    volume -= 1
                } label: {
                    Image(systemName: "speaker.minus.fill")
                }
            }
            .buttonStyle(.borderless)
            .padding(10)
            .background(RoundedRectangle(cornerRadius: 10).fill(Color.black))
            .foregroundStyle(Color.white)
        }
    }
    .padding(.vertical, 20)
    .padding(.horizontal, 10)
    // 定义遥控器高度,并且设置遥控器底部对其
    .frame(height: 200, alignment: .bottom)
    .background(
        // 定义遥控器灰色背景
        RoundedRectangle(cornerRadius: 10)
            .fill(Color.black.opacity(0.5))
    )
    .foregroundStyle(Color.white)
CleanShot 2024-10-28 at 22.29.40@2x.png

看起来有点多,但是其实不复杂,相信你可以理解!哈哈哈!

这样看点击是没有没有问题的,现在我们将我们创建的遥控器抽离出来作为当做的视图,并且重命名为 RemoteControlView

CleanShot 2024-10-28 at 22.32.10.gif

重命名为RemoteControlView CleanShot 2024-10-28 at 22.34.30@2x.png

CleanShot 2024-10-28 at 22.33.43.gif

你可以看到,目前报错了,预览那里也没办法正常预览,报错信息在我们刚才分离出来的视图那里,主要提示为:Cannot find 'volume' in scope

当前视图作用域内没有找到 volume, 这是因为我们创建新的视图,目前不在之前的作用域空间内!

解决这个问题,因为我们可以在我们抽离出来的视图中重新定义新的状态,例如这样:

CleanShot 2024-10-28 at 22.39.48@2x.png

这样就可以解决我们的报错的问题,但是你可能发现,我点击我遥控器,电视音量没有变化,这是正确的!

因为目前电视跟遥控器都是有独立的音量状态,他们并没有进行关联!

如果需要进行关联双向的绑定,我们就需要用到 @Binding

我们将 RemoteControllView 视图中的 @State 更换为 @Binding, 同时 在 StateAndBindingBootcamp 中的 RemoteControllView调用处 传入电视的音量状态

RemoteControllView(newVolume: $volume)
CleanShot 2024-10-28 at 22.45.52@2x.png

这样我们就可以将 电视的音量状态同步到我们遥控上的音量控制,同时遥控器的音量控制也可以影响到电视的音量状态,这就是数据的双向绑定了!

但是记住数据的双向绑定需要使用 $ 符号 标识变量

同时记住 @Binding 修饰器修饰的变量不能使用 private 标记为私有!

CleanShot 2024-10-28 at 22.48.56@2x.png

好的,恭喜你,如果您看到到这里你也许已经完成了当前案例或者有些掌握这两的概念!

你现在可以试试这两遥控器控制电视音量的变化效果!

持续进步!

这是全部代码!

import SwiftUI

struct StateAndBindingBootcamp: View {
    @State private var volume: Int = 5  // 音量状态,独立在电视内
    var body: some View {
        VStack {
            Text("当前电视音量🔊:\(volume)")
                .foregroundStyle(Color.white)
                .padding(.bottom, 10)
                .background(alignment: .bottom) {
                    RoundedRectangle(cornerRadius: 10)
                        .frame(width: 250, height: 200, alignment: .center)
                }
            // 遥控器
            RemoteControllView(newVolume: $volume)
            // 电视自身音量控制按钮
            HStack {
                // 使用 Group 包裹给包裹元素统一运用修饰符
                Group {
                    Button {
                        volume += 1
                    } label: {
                        Image(systemName: "speaker.plus.fill")
                    }
                    Button {
                        volume -= 1
                    } label: {
                        Image(systemName: "speaker.minus.fill")
                    }
                }
                .buttonStyle(.borderless)
                .padding(10)
                .background(Circle().fill(Color.blue))
                .foregroundStyle(Color.white)
            }
        }
    }
}
#Preview {
    StateAndBindingBootcamp()
}

struct RemoteControllView: View {
    @Binding var newVolume: Int  // 音量状态,独立在遥控器内
    var body: some View {
        VStack {
            Text("遥控器")
                .font(.title3)
                .fontWeight(.semibold)
            HStack {
                // 使用 Group 包裹给包裹元素统一运用修饰符
                Group {
                    Button {
                        newVolume += 1
                    } label: {
                        Image(systemName: "speaker.plus.fill")
                    }
                    Button {
                        newVolume -= 1
                    } label: {
                        Image(systemName: "speaker.minus.fill")
                    }
                }
                .buttonStyle(.borderless)
                .padding(10)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.black))
                .foregroundStyle(Color.white)
            }
        }
        .padding(.vertical, 20)
        .padding(.horizontal, 10)
        // 定义遥控器高度,并且设置遥控器底部对其
        .frame(height: 200, alignment: .bottom)
        .background(
            // 定义遥控器灰色背景
            RoundedRectangle(cornerRadius: 10)
                .fill(Color.black.opacity(0.5))
        )
        .foregroundStyle(Color.white)
    }
}

如果本专栏对你有帮助,不妨点赞、评论、关注~