在 SwiftUI 中,我如何知道 Picker 选择何时更改?为啥 didSet 不起作用?
Posted
技术标签:
【中文标题】在 SwiftUI 中,我如何知道 Picker 选择何时更改?为啥 didSet 不起作用?【英文标题】:In SwiftUI, how do I know when a Picker selection was changed? Why doesn't didSet work?在 SwiftUI 中,我如何知道 Picker 选择何时更改?为什么 didSet 不起作用? 【发布时间】:2019-11-05 22:08:59 【问题描述】:我在 SwiftUI Form
中有一个 Picker
,并且我正在尝试在选择器更改时处理该值。我希望能够在代表当前选定值的变量上使用didSet
来执行此操作。
import SwiftUI
struct ContentView: View
enum TransmissionType: Int
case automatic
case manual
@State private var selectedTransmissionType: Int = TransmissionType.automatic.rawValue
didSet
print("selection changed to \(selectedTransmissionType)")
private let transmissionTypes: [String] = ["Automatic", "Manual"]
var body: some View
NavigationView
Form
Section
Picker(selection: $selectedTransmissionType,
label: Text("Transmission Type"))
ForEach(0 ..< transmissionTypes.count)
Text(self.transmissionTypes[$0])
Picker
UI (大部分)按预期工作:我可以看到默认值已被选中,点击选择器,它会打开一个新视图,我可以选择其他值,然后返回主表单并显示新值已被选中。但是,永远不会调用 didSet
。
我看到了this question,但在我的View
代码中添加更多内容而不是仅在变量更改时处理新值(如果可能的话)似乎很奇怪。使用onReceive
是否更好,即使它会导致更复杂的视图?我的主要问题是:阻止调用 didSet
的代码有什么问题?
我使用this example 来达到这一点。
除了我的常规问题之外,我还有一些关于此示例的其他问题:
A) 我有一个enum
和一个Array
来表示相同的两个值,这似乎很奇怪。有人还可以建议一种更好的方法来构建它以避免这种冗余吗?我考虑过 TransmissionType
对象,但与 enum
相比,这似乎有点过分了……也许不是?
B) 当点击选择器时,带有选择器选项的屏幕会滑过,然后两个选项会向上跳跃一点。这让人感觉刺耳、紧张和糟糕的用户体验。我在这里做错了什么导致糟糕的用户体验吗?或者它可能是一个 SwiftUI 错误?每次更改选择器时都会出现此错误:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.
【问题讨论】:
【参考方案1】:我早些时候开始输入这个,然后回来发现 LuLuGaGa 打败了我。 :D 但是反正我有这个...
主要问题:来自Swift Language Guide:
"当你给一个存储的属性分配一个默认值,或者设置它的 初始化器中的初始值,该属性的值被设置 直接,无需调用任何属性观察者。”
所以当视图被构建时属性观察器不会被触发。但是当@State
变量发生变化时,会构造一个新的视图实例(请记住,视图是结构或值类型)。因此,didSet
属性观察器实际上对@State
属性没有用处。
您要做的是创建一个符合ObservableObject
的类,并使用@ObservedObject
属性包装器从您的视图中引用它。因为该类存在于结构之外,所以您可以在其属性上设置属性观察器,它们会像您期望的那样触发。
问题 A:如果您使枚举符合 CaseIterable
,则可以只使用枚举(参见下面的示例)
问题 B:据我所知,这似乎是一个 SwiftUI 错误,因为在 NavigationView
/Form
组合中的任何 Picker
都会发生这种情况。我推荐reporting it to Apple。
这是我将如何删除枚举和数组的冗余,并将选择保存在 UserDefaults 中:
extension ContentView
// CaseIterable lets us use .allCases property in ForEach
enum TransmissionType: String, CaseIterable, Identifiable, CustomStringConvertible
case automatic
case manual
// This lets us omit 'id' parameter in ForEach
var id: TransmissionType
self
// This just capitalizes the first letter for prettier printing
var description: String
rawValue.prefix(1).uppercased() + rawValue.dropFirst()
class SelectionModel: ObservableObject
// Save selected type to UserDefaults on change
@Published var selectedTransmissionType: TransmissionType
didSet
UserDefaults.standard.set(selectedTransmissionType.rawValue, forKey: "TransmissionType")
// Load selected type from UserDefaults on initialization
init()
if let rawValue = UserDefaults.standard.string(forKey: "TransmissionType")
if let transmissionType = TransmissionType(rawValue: rawValue)
self.selectedTransmissionType = transmissionType
return
// Couldn't load from UserDefaults
self.selectedTransmissionType = .automatic
那么你的视图就像
struct ContentView: View
@ObservedObject var model = SelectionModel()
var body: some View
NavigationView
Form
Section
Picker(selection: $model.selectedTransmissionType, label: Text("Transmission Type"))
ForEach(TransmissionType.allCases) type in
Text(type.description)
【讨论】:
完美解释??【参考方案2】:这里有两个问题。
1) 如果标题样式为“.inline”,则可以解决跳转问题。
2) OnReceive() 是用组合框架方法替换didSet
请求的最简单方法之一,这是SwiftUI 的核心技术。
struct ContentView: View
enum TransmissionType: Int
case automatic
case manual
@State private var selectedTransmissionType: Int = TransmissionType.automatic.rawValue
didSet
print("selection changed to \(selectedTransmissionType)")
private let transmissionTypes: [String] = ["Automatic", "Manual"]
var body: some View
NavigationView
Form
Section
Picker(selection: $selectedTransmissionType,
label: Text("Transmission Type"))
ForEach(0 ..< transmissionTypes.count)
Text(self.transmissionTypes[$0])
.navigationBarTitle("Title", displayMode: .inline) // this solves jumping and all warnings.
.onReceive(Just(selectedTransmissionType)) value in
print(value) // Just one step can monitor the @state value.
【讨论】:
我喜欢使用 Combine 来读取@State
变量的变化。 +1 不幸的是,虽然displayMode: .inline
没有出现大的垂直跳跃,但是当你第一次打开Picker
时仍然存在水平抖动,所以这仍然是一个需要报告的错误。
感谢有关.inline
的提示,但我在主导航视图上有一个大标题,当我制作这个 .inline` 时它仍然有问题,除非我也将它设置为 @987654327 @。看来这一定是 SwiftUI 中的错误【参考方案3】:
didSet 不会在 @State 上被调用,因为它是一个包装器 - 您在状态中设置一个值而不是状态本身。重要的问题是你为什么想知道它已经设置好?
你的观点可以简化一点:
如果您将枚举声明为具有原始字符串类型,则不需要名称数组。如果您将其声明为 CaseIterable,您将能够通过调用 Array(TransmissionType.allCases) 来获取所有案例。如果它也被声明为可识别,您将能够将所有案例直接传递到 ForEach。接下来您需要将 rawValue 传递到文本中,并记住在其上放置标签以便进行选择:
struct ContentView: View
enum TransmissionType: String, CaseIterable, Identifiable
case automatic
case manual
var id: String
return self.rawValue
@State private var selectedTransmissionType = TransmissionType.automatic
var body: some View
NavigationView
Form
Section
Picker(selection: $selectedTransmissionType,
label: Text("Transmission Type"))
ForEach(Array(TransmissionType.allCases))
Text($0.rawValue).tag($0)
我看不出奇怪的跳跃从何而来 - 你是否也在设备上复制了它?
【讨论】:
谢谢!我想知道值何时更改的原因是我可以将其保存在 UserDefault 中以在应用程序启动之间保存。另外,是的,跳跃发生在设备上。 在这种情况下,您必须使用 ObjectBinding 而不是 State,因为这不是视图本地的变量以上是关于在 SwiftUI 中,我如何知道 Picker 选择何时更改?为啥 didSet 不起作用?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 SwiftUI 中使用多个 Picker 更改 UIPickerView 宽度?
如何让 SwiftUI Picker 在子视图中工作? (变灰)