本文从TextField失效问题出发,实际的问题本质涉及SwiftUI的状态绑定和视图刷新机制,逐步排查和解决绑定目标失效问题。
问题复现
在iOS应用中,当点击TextField输入框时,发现输入的内容实际并不会显示,点击图标也没有反应,也就意味着并没有绑定成功。
![](https://fangjunyu.com/wp-content/uploads/2025/01/1-10.gif)
但是,当我在创建存钱罐时,如果点击“名称”输入框后,即使“名称”输入框没有内容,其他的输入框就可以输入内容并赋值成功,图标也可以完成赋值。
![](https://fangjunyu.com/wp-content/uploads/2025/01/2-7.gif)
问题定位
// 存钱罐名称输入框
TextField("Set the name of the piggy bank", text: $piggyBankData.name)
.focused($isFocus, equals: .nameField)
.onChange(of: piggyBankData.name) { _,newValue in
print("newValue:\(newValue)")
if newValue.count > limitLength {
piggyBankData.name = String(newValue.prefix(limitLength))
}
}
这是存钱罐名称的输入框代码,起初以为是focused聚焦有问题,导致其他的输入框无法显示。
经过排查时,将focused和onChange都隐藏后,仍然存在只有点击“名称”输入框后,其他的输入框才能输入内容。
TextField("Set the name of the piggy bank", text: $piggyBankData.name)
// .focused($isFocus, equals: .nameField)
// .onChange(of: piggyBankData.name) { _,newValue in
// print("newValue:\(newValue)")
// if newValue.count > limitLength {
// piggyBankData.name = String(newValue.prefix(limitLength))
// }
// }
因此,问题不在focused和onChange,而是在与名称有关的文本输入框中。
但是代码中又没有设置过限制,因此又拿出其他的文本输入框代码进行对比:
// 设置存钱罐金额
HStack {
Text("Amount")
TextField("0.0", text: Binding(
get: { piggyBankData.targetAmount == 0 ? "" : String(piggyBankData.targetAmount) },
set: { piggyBankData.targetAmount = Double($0) ?? 0 }
))
.focused($isFocus, equals: .amountField)
.keyboardType(.decimalPad)
.submitLabel(.continue)
}
对比发现,其他的输入框因为涉及条件的判断,都使用的是Binding,而名称输入框没有条件的限制,因此,没有在名称输入框中使用Binding:
TextField("Set the name of the piggy bank", text: $piggyBankData.name)
因此,问题初步定位在Binding无法完成赋值。
再退一步,需要了解为什么使用piggyBankData。在SwiftUI当中,创建存钱罐时因为区分了两个页面,因此需要一个内容的传递。这个内容的传递就是ContentView视图中创建了一个piggyBankData对象。
当创建存钱罐名称时,将名称和金额绑定到外层ContentView视图的piggyBankData对象,下一步的图标和初始金额也同理,都绑定到piggyBankData对象上,在下一步是确认视图,如果确认则插入到SwiftData中。如果需要重新创建,则返回到第一个创建视图。
![](https://fangjunyu.com/wp-content/uploads/2025/01/3-28.png)
这里可以看一下ContentView的代码:
import SwiftUI
struct ContentView: View {
@AppStorage("pageSteps") var pageSteps: Int = 1
@State private var piggyBankData: PiggyBankData? = PiggyBankData()
var body: some View {
if pageSteps == 1 {
WelcomeView(pageSteps: $pageSteps)
} else if pageSteps == 2 {
PrivacyPage(pageSteps: $pageSteps)
} else if pageSteps == 3 {
CreatePiggyBankPage1(pageSteps: $pageSteps, piggyBankData: Binding(
get: { piggyBankData ?? PiggyBankData() },
set: { piggyBankData = $0 })
)
} else if pageSteps == 4 {
CreatePiggyBankPage2(pageSteps: $pageSteps,piggyBankData: Binding(
get: { piggyBankData ?? PiggyBankData() },
set: { piggyBankData = $0 })
)
} else if pageSteps == 5 {
CompletedView(pageSteps: $pageSteps,piggyBankData: Binding(
get: { piggyBankData ?? PiggyBankData() },
set: { piggyBankData = $0 })
)
}
else {
Home()
.onAppear {
piggyBankData = nil
}
}
}
}
在ContentView视图中,根据pageSteps值显示相应的视图。当pageSteps为3时,显示创建视图1,当pageSteps为4时,显示创建视图2。
两个创建视图是根据ContentView进行显示的,但是如果不点击“名称”输入框,创建视图2也会出现无法选择图标、输入框无法写入的情况。
因此,推测问题在于piggyBankData为nil后,创建视图中输入Binding的文本框就会出现无法赋值的情况。
解决方案
因此,就把piggyBankData不赋值为nil,在Home视图显示时,piggyBankData赋值为PiggyBankData()。
Home()
.onAppear {
piggyBankData = PiggyBankData()
}
重新测试,不点击“名称”输入框,其他的输入框仍然可以实现赋值,问题得到解决。
![](https://fangjunyu.com/wp-content/uploads/2025/01/4-6.gif)
为什么需要设置piggyBankData为nil?
在设计创建视图时,我考虑的是创建一个临时存钱罐,当所有创建视图创建完成后,回到主视图时,临时存钱罐变为nil,这样就可以防止创建新的存钱罐时,旧的存钱罐信息残留。
![](https://fangjunyu.com/wp-content/uploads/2025/01/5-9.png)
这里涉及到一个如果piggyBankData为nil,则创建一个piggyBankData对象:
CreatePiggyBankPage1(pageSteps: $pageSteps, piggyBankData: Binding(
get: { piggyBankData ?? PiggyBankData() },
set: { piggyBankData = $0 })
)
但实际上这个代码并未奏效,这里涉及到Binding的行为方式。
Binding 是如何工作的?
1、Binding 的本质
一个 Binding 是某个值的引用,它直接链接到一个源数据。
Binding 的 get 是用来获取源数据的值,而 set 是用来更新源数据的。
2、get 的行为
当 SwiftUI 渲染视图并需要访问绑定数据时,它会调用 get 方法来获取值。
如果 get 方法返回的是 nil,那么绑定值将被视为无效,SwiftUI 的相关视图可能无法正常工作。
3、Binding(get:set:) 是显式绑定
通过 get 和 set 创建的显式绑定,只会在调用时动态计算值。
如果绑定的源数据(如 piggyBankData)本身是 nil,get 返回的就是一个新的临时实例。
为什么 get 没有完成初始化?
在代码中:
CreatePiggyBankPage1(pageSteps: $pageSteps, piggyBankData: Binding(
get: { piggyBankData ?? PiggyBankData() },
set: { piggyBankData = $0 }
))
get 的返回值是:
如果 piggyBankData 为非 nil,它返回当前的 piggyBankData。
如果 piggyBankData 为 nil,它会返回一个新的 PiggyBankData() 实例。
问题的核心在于:SwiftUI 渲染视图时何时调用 get,以及调用后对视图的影响。
问题点 1:视图加载时绑定的时机
SwiftUI 的视图是基于数据驱动的,但它会延迟访问数据(懒加载)。
如果在渲染 CreatePiggyBankPage1 时,piggyBankData 是 nil,get 会返回一个新的 PiggyBankData(),但这个值只存在于绑定中,并没有实际写回到 @State piggyBankData。
![](https://fangjunyu.com/wp-content/uploads/2025/01/6-8.png)
问题点 2:Binding 的值没有持久性
get 返回的临时 PiggyBankData() 没有持久化到 piggyBankData。
由于 Binding 是动态计算的,每次访问都会返回一个新的实例(因为 piggyBankData 始终是 nil),导致绑定的值无法保持一致。
![](https://fangjunyu.com/wp-content/uploads/2025/01/7-7.png)
为什么在 onAppear 初始化可以解决问题?
.onAppear {
piggyBankData = PiggyBankData()
}
当在 onAppear 中显式初始化 piggyBankData 时:
1、piggyBankData 被设置为一个非 nil 的有效实例。
2、此时 Binding 的 get 方法总是返回这个实例,而不再创建临时对象。
3、视图绑定的值变得稳定,因为 Binding 的值来源于一个真实的持久数据(即 @State piggyBankData)。
两个TextField的区别
1、第一个 TextField
TextField("Set the name of the piggy bank", text: $piggyBankData.name)
text 是直接绑定到 piggyBankData.name。
TextField 会直接修改 name 的值,当它被点击或触发时,SwiftUI 会通知 @Binding 的父视图重新计算 piggyBankData 的状态。
![](https://fangjunyu.com/wp-content/uploads/2025/01/8-7.png)
2、第二个 TextField
TextField("0.0", text: Binding(
get: { piggyBankData.targetAmount == 0 ? "" : String(piggyBankData.targetAmount) },
set: { piggyBankData.targetAmount = Double($0) ?? 0 }
))
这里使用了自定义的 Binding,通过 get 和 set 逻辑与 piggyBankData.targetAmount 进行映射。
自定义 Binding 的更新行为取决于 piggyBankData 的完整性。如果 piggyBankData 没有被正确初始化或未触发视图刷新,get 和 set 的逻辑不会被调用。
问题的本质
问题源于 TextField 是否触发了 piggyBankData 的绑定更新:
1、@Binding 的懒加载机制
@Binding 的值更新依赖于 SwiftUI 数据流。如果某些属性未被初始化或未在视图中显式访问,绑定的更新行为可能不会触发。
第一个 TextField 直接绑定到 piggyBankData.name,当点击时,SwiftUI 确保了 piggyBankData 的完整性,并触发了绑定刷新。
2、自定义 Binding 的特殊行为
自定义 Binding 的 get 和 set 是懒加载的,它们不会主动触发视图更新。只有在 piggyBankData 状态被更新或明确访问时,get 和 set 才会执行。
如果 piggyBankData.name 的绑定未被访问,targetAmount 的自定义绑定可能无法触发刷新。
总结
问题本质上涉及 SwiftUI 的 状态绑定 和 视图刷新机制。通过在 onAppear 中重新初始化 piggyBankData,实际上确保了绑定的 State 在 SwiftUI 的渲染流程中处于正确的状态。
.onAppear {
piggyBankData = PiggyBankData()
}
回过头来看一下,为什么第一个TextField的行为可以在piggyBankData为nil时,仍然可用?因为 TextField 的行为和绑定机制有一个“隐式初始化”的作用。
TextField 是一个视图,它通过 Binding 将用户输入直接映射到某个数据模型上:
TextField("Placeholder", text: $bindingValue)
1、TextField 的绑定
text 参数接受一个 Binding<String>。
它通过 Binding 的 get 和 set 方法访问底层数据。
get:读取数据并显示到 TextField。
set:当用户输入时,将数据写回。
2、Binding 的动态行为
如果绑定的底层数据是 nil(比如 piggyBankData 是可选类型并且当前为 nil),SwiftUI 会触发绑定的 set 方法,只要用户对 TextField 进行了编辑,数据就会被写回,从而间接地完成了初始化。
为什么 TextField 会触发 piggyBankData 的初始化?
在代码中:
TextField("Set the name of the piggy bank", text: $piggyBankData.name)
piggyBankData 是一个可选类型(PiggyBankData?)。
$piggyBankData.name 的绑定背后,本质上是访问了 Binding(get:set:) 的方法:
Binding(
get: { piggyBankData?.name ?? "" },
set: { newValue in
if piggyBankData == nil {
piggyBankData = PiggyBankData()
}
piggyBankData?.name = newValue
}
)
具体步骤如下:
1、读取时:
当 TextField 显示时,SwiftUI 会调用 get 方法。
如果 piggyBankData == nil,get 返回的是 “”(空字符串),TextField 显示为空。
2、写入时:
当用户在 TextField 中输入时,SwiftUI 会调用 set 方法。
set 方法发现 piggyBankData == nil,于是创建了一个新的 PiggyBankData 实例。
然后,用户的输入被写入到 piggyBankData.name 中。
因此,TextField 的行为隐式地完成了对 piggyBankData 的初始化任务。
TextField 能完成初始化,是因为其绑定逻辑中触发了 set 方法,而 set 方法检查到 piggyBankData == nil 时创建了新的实例。这种行为依赖于绑定和用户交互,但并非显式设计,可能导致隐式依赖和难以排查的 bug。通过显式初始化或改进绑定逻辑,可以使代码更清晰且更具可维护性。