TimeDataSource<Date>.currentDate 它不是一个普通的
Date,系统当前时间的数据源(Current Time Source)import SwiftUI
struct ElapsedTime: View {
let startTime: Date = Date.now
var body: some View {
Text(TimeDataSource<Date>.currentDate, format: .offset(to: startTime, allowedFields: [.hour, .minute, .second]))
}
}
#Preview {
ElapsedTime()
}以前写一个计时器通常需要:
@State private var now = Date()
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
now = Date()
}
// 或
TimelineView(.periodic(from: .now, by: 1))。protocol像struct,但是没有实现。其实像Java里的Interface,C++里的纯抽象类。
protocol View {
var body: some View { get }
}这里没有任何实现,大括号里只说了get,意味着这是一个只读变量。
struct ContentView: View { ... }我们是在承诺实现名为 View 的协议中的所有变量和函数。协议可以根据需要包含任意多的变量和函数。
为什么需要协议
extension View {
func font(_ font: Font) -> some View {
// 创建并返回一个自身已修改为具有给定字体的视图
}
}some 关键字对调用方或被调用方隐藏了具体类型。
当 some 用于返回值类型时,它对调用方隐藏了具体类型。 当 some 用于参数类型时,它对被调用方隐藏了具体类型。
在任何情况下,编译器在编译时都知道该类型。例如,var body 的返回类型是 some View。 实际返回的类型由 var body 中的代码决定(即由被调用方决定)。 调用方只知道它遵循 View 协议。
当 some 用于参数类型时,只有调用方知道该类型。 被调用方只能使用协议中的函数/变量(这类似于泛型)。
例如,动画视图修饰符中 value 参数的类型:
func animation(Animation?, value: some Equatable)想象一个由实现同一协议的不同具体类型组成的集合:
let views: [any View] = [PegView(), CodeView()]我们知道遵循协议会带来功能上的收益(例如视图修饰符)。但我们也使用协议来约束其他类型,以确保它们按照我们的意愿运行。这对于泛型中的“不在乎(don’t care)”类型尤为重要。
Swift 中 Dictionary 的声明方式:
struct Dictionary<Key, Value> where Key: HashableDictionary 结构体约束其 Key(即“不在乎”的部分)必须遵循 Hashable 协议。
where 子句使得原本的“不在乎”现在变成了“有一定要求”。只能拥有键为 Hashable 的字典。
Equatable 协议中仅包含一个函数:
protocol Equatable {
static func == (lhs: Self, rhs: Self) -> Bool
}Swift 中的 == 运算符知道调用此静态(即全局)函数,以比较该类型的两个对象。 大写的 Self 表示“当前正在遵循该协议的类型”。以 Kind 枚举为例的函数声明如下:
static func == (lhs: Kind, rhs: Kind) -> BoolArray 的 Equatable 实现
extension Array: Equatable where Element: Euqatable {
static func == (lhs: Array<Element>, rhs: Array<Element>) -> Bool {
lhs.count == rhs.count && lhs.indices.allSatisfy {
lhs[$0] == rhs[$0] }
}
}在 SwiftUI 中,将某事物设为 Equatable 的一个好处是可以使用
.onChange(of:) {}其中的 of: 参数必须是 Equatable的。
SwiftUI 会反复检查该参数是否与其之前的状态 == (相等)。
import SwiftUI
struct ElapsedTime: View {
@State private var password = ""
@State private var passwordStrength = "请输入密码"
var body: some View {
VStack {
Form {
Section(header: Text("账号设置")) {
SecureField("输入新密码", text: $password)
}
Section(header: Text("密码强度检查")) {
Text(passwordStrength)
.foregroundColor(strengthColor)
.bold()
}
}
}
// 👇 这里是核心:只要 password 变量发生变化,就会触发这个闭包
.onChange(of: password) { oldValue, newValue in
checkPasswordStrength(newValue)
}
}
// 简单的强度检查逻辑
private func checkPasswordStrength(_ pass: String) {
if pass.isEmpty {
passwordStrength = "请输入密码"
} else if pass.count < 6 {
passwordStrength = "太短了(至少6位)"
} else if pass.count < 10 {
passwordStrength = "中等强度"
} else {
passwordStrength = "强密码 💪"
}
}
// 根据强度返回不同的颜色
private var strengthColor: Color {
switch passwordStrength {
case "强密码 💪": return .green
case "中等强度": return .orange
case "太短了(至少6位)": return .red
default: return .gray
}
}
}
#Preview {
ElapsedTime()
}可哈希性(Hashability),Hashable(可哈希)是一个协议,用于加速在集合中查找 Equatable(可等价性)元素的速度。两个通过 == 判断为相等的对象,绝不能“哈希”出不同的值。
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}任何表现得像 Hashable 的对象,也必须表现得像 Equatable。
let dict = [
"apple": 1,
"banana": 2
]为什么查找 apple 很快,因为 “apple” 会变成一个 hash value,直接定位 bucket,而不是一个个比较。
对于 struct Hasher 内部实现了一种哈希算法
var hasher = Hasher()
hasher.combine(100)
hasher.combine("Tom")
let value = hasher.finalize() // IntSwift 为了防止 Hash Collision Attack(哈希碰撞攻击),每次程序启动都会随机初始化哈希种子(seed)。不要把 hash 值保存到文件或数据库,更不要用于网络协议。
你的 SwiftUI 界面是由 Model(模型)数据驱动 的。为了让 UI 与数据保持同步,SwiftUI 经常需要能够识别(identify) Model 中的数据。随后,它会根据每个数据对象的身份(Identity),把 View 与 Model 中对应的数据关联起来。
身份(Identity)对于 动画(Animation) 来说可能是最重要的概念。随着 Model 数据发生变化,View 会移动、出现或者消失,以反映这些变化。如果 Model 中的数据没有唯一身份,UI 就很难追踪哪些元素发生了变化。
一个最典型、也是大家最熟悉的例子,就是 ForEach。
Identity 是 SwiftUI Diff(差异比较)算法的基础,没有 Identity,就没有高效更新,也没有流畅动画。有点像 React.js
你把一个 集合(Collection) 传给 ForEach,它会为集合中的每一个元素创建一个 View。
当集合发生变化时,ForEach 会努力保持它创建的 View 与集合中的元素一一对应(匹配)。
也就是说:
而不是把所有 View 全部重建。高效的差异化更新。
因此,ForEach 要求你必须为集合中的每一个元素提供一个唯一且稳定(stable)的标识符(Identifier)。
方式一:使用 id 参数,为集合中的每个元素指定一个能够返回其唯一标识符的属性。
id: \.self这里的 self 是一个特殊的属性,它表示:集合中的元素本身。这种写法只有满足以下条件才能工作:
集合中的元素本身必须:
如果你使用 数组索引 作为元素的 Identity,只有唯一 和 遵守Hashable,稳定性需要你自己保证,绝不能重新排序底层数组,也不能 在数组中间删除元素,在数组中间插入元素,唯一允许的是 只能在数组末尾追加、只能删除数组最后一个元素。
如果一个集合中的元素不是唯一的(unique)、不是稳定的(stable),或者本身不符合 Hashable(而不像数组索引那样)怎么办?
那么你不能使用:id: \.self
不过你仍然可以使用:id: \.someOtherVar
也就是说,如果你的元素中有某个属性:是 Hashable 能唯一标识该元素
那么就可以把它作为 id。
方式二:元素实现 Identifiable
protocol Identifiable {
associatedtype ID: Hashable
var id: Self.ID
}struct Todo: Identifiable {
let id = UUID()
var title: String
}
ForEach(todos) { todo in
Text(todo.title)
}它就是一个面向对象中的 类(class),和你在其他编程语言里使用的 class 完全一样。
它支持单继承(single inheritance)。在 SwiftUI 中基本不会使用继承。
SwiftUI 更推荐,Protocol、Struct、Composition,而不是
View
├── MyButton
│ ├── RedButton
│ ├── BigRedButton和struct唯一真正巨大的区别是,class是一个引用类型 而不是 值类型。
Tracking changes(跟踪变化),SwiftUI 会跟踪 Model 中的所有变化,并确保 UI 始终反映最新状态。
对于 struct 来说,这很容易,因为 Swift 完全知道结构体什么时候发生了修改。(这也是 Swift 实现 Copy-on-Write(写时复制) 等机制的基础。)
但对于 class,情况通常不同,因为 Swift 并不知道类实例内部什么时候发生了修改。不过 SwiftUI 提供了一个宏,它会包装(wrap)你的类中的属性,并在属性变化时通知 SwiftUI。
struct 自己就会产生新的值,所以 SwiftUI 能检测到变化; class
永远是同一个对象,所以 SwiftUI 不知道里面什么时候改了,因此需要
@Observable 告诉它。
struct User {
var name: String
}
@State private var user = User(name: "Tom")
user.name = "Jerry"虽然看起来只是改了一个属性。实际上发生的是:旧User被新的User整个替换,因为 struct 是 Value Type(值类型)。SwiftUI 能够发现 user 这个值变了,然后重新计算 body,整个过程不用观察对象。
class User {
var name = "Tom"
}
@State private var user = User()
user.name = "Jerry"修改user内部,user指针没变,对象地址没变。SwiftUI 看见的是,user没变啊,于是 body 不刷新,它不知道 对象里面偷偷改了。
import Observation
@Observable
class User {
var name = "Tom"
}编译器会偷偷帮你生成很多代码
class User {
private let registrar = ObservationRegistrar()
var name: String {
get {
registrar.access(...)
return _name
}
set {
registrar.withMutation(...) {
_name = newValue
}
}
}
}