SwiftUI DatePicker 绑定可选日期,有效 nil

SwiftUI DatePicker Binding optional Date, valid nil

提问人:wk.experimental 提问时间:12/11/2019 更新时间:4/11/2023 访问量:8626

问:

我正在试验来自 https://alanquatermain.me/programming/swiftui/2019-11-15-CoreData-and-bindings/ 的代码

我的目标是让 DatePicker 绑定到 Binding< Date?>允许 nil 值而不是启动 Date();如果您的核心数据模型实体中有接受 nil 作为有效值的 Date 属性,这将非常有用。

这是我的 swift playground 代码:

extension Binding {
    init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
        self.init(get: { source.wrappedValue != nil },
                  set: { source.wrappedValue = $0 ? defaultValue : nil})
    }
}

struct LiveView: View {
    @State private var testDate: Date? = nil
    var body: some View {
        VStack {
            Text("abc")

            Toggle("Has Due Date",
                   isOn: Binding(isNotNil: $testDate, defaultValue: Date()))

            if testDate != nil {
                DatePicker(
                    "Due Date",
                    selection: Binding($testDate)!,
                    displayedComponents: .date
                )
            }
        }
    }
}

let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)

我找不到修复此代码的解决方案。当切换开关首次切换到打开时,它有效,但当切换开关重新关闭时,它会崩溃。

当我删除 DatePicker 并将代码更改为以下内容时,代码似乎表现正常:

extension Binding {
    init<T>(isNotNil source: Binding<T?>, defaultValue: T) where Value == Bool {
        self.init(get: { source.wrappedValue != nil },
                  set: { source.wrappedValue = $0 ? defaultValue : nil})
    }
}

struct LiveView: View {
    @State private var testDate: Date? = nil
    var body: some View {
        VStack {
            Text("abc")

            Toggle("Has Due Date",
                   isOn: Binding(isNotNil: $testDate, defaultValue: Date()))

            if testDate != nil {
                Text("\(testDate!)")
            }
        }
    }
}

let liveView = LiveView()
PlaygroundPage.current.liveView = UIHostingController(rootView: liveView)

我怀疑这与代码的这一部分有关

DatePicker("Due Date", selection: Binding($testDate)!, displayedComponents: .date )

当 source.wrappedValue 设置回 nil 时出现问题(请参阅绑定扩展)

核心数据 SwiftUI Swift-Playground

评论


答:

22赞 Asperi 12/11/2019 #1

问题是,由于动作,即使您将其从视图中删除,抓取绑定也不会那么快释放它,因此它在强制展开可选时崩溃,这变得零......DatePickerToggle

此崩溃的解决方案是

DatePicker(
    "Due Date",
    selection: Binding<Date>(get: {self.testDate ?? Date()}, set: {self.testDate = $0}),
    displayedComponents: .date
)

评论

1赞 wk.experimental 12/11/2019
您的解决方案修复了崩溃,但未按预期运行,因为我们希望 DatePicker 修改 testDate 的值。在代码中,当切换打开时,DatePicker 选择将绑定到 .constant(Date()) 而不是 testDate。但是,我认为您是对的,由于强制解开 nil 可选而导致的崩溃,似乎当切换关闭 DatePicker 时仍在尝试解包可选(现在设置为 nil),我希望代码不会这样做,因为我已经检查了 testdate != nil 是否不正确。仍在寻找解决此问题的解决方案。
0赞 Asperi 12/11/2019
更新了另一个不可崩溃的变体
4赞 andrewbuilder 12/1/2021 #2

我在所有 SwiftUI 选择器中使用的替代解决方案......

我通过阅读 Jim Dovey博客了解了几乎所有关于 SwiftUI 绑定(带有核心数据)的知识。剩下的是一些研究和相当多的错误时间的结合。

因此,当我使用 Jim 的技术在 SwiftUI 上创建时,我们最终会得到这样的东西,用于取消选择为零......ExtensionsBinding

public extension Binding where Value: Equatable {
    init(_ source: Binding<Value>, deselectTo value: Value) {
        self.init(get: { source.wrappedValue },
                  set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
        )
    }
}

然后可以像这样在整个代码中使用它......

Picker("Due Date", 
       selection: Binding($testDate, deselectTo: nil),
       displayedComponents: .date
) 

或者使用时.pickerStyle(.segmented)

Picker("Date Format Options", // for example 
       selection: Binding($selection, deselectTo: -1)) { ... }
    .pickerStyle(.segmented)

...它将分段样式选取器的 -1 设置为 -1,根据 和 selectedSegmentIndex 的文档。indexUISegmentedControl

默认值为 noSegment(未选择段),直到用户 触摸一个段。将此属性设置为 -1 可关闭当前 选择。

1赞 Fiser 6/24/2022 #3

这是我的解决方案,我添加了一个按钮来删除日期并添加默认日期。所有这些都包含在一个新组件中

https://gist.github.com/Fiser12/62ef54ba0048e5b62cf2f2a61f279492

import SwiftUI

struct NullableBindedValue<T>: View {
    var value: Binding<T?>
    var defaultView: (Binding<T>, @escaping (T?) -> Void) -> AnyView
    var nullView: ( @escaping (T?) -> Void) -> AnyView

    init(
        _ value: Binding<T?>,
        defaultView: @escaping (Binding<T>, @escaping (T?) -> Void) -> AnyView,
        nullView: @escaping ( @escaping (T?) -> Void) -> AnyView
    ) {
        self.value = value
        self.defaultView = defaultView
        self.nullView = nullView
    }

    func setValue(newValue: T?) {
        self.value.wrappedValue = newValue
    }

    var body: some View {
        HStack(spacing: 0) {
            if value.unwrap() != nil {
                defaultView(value.unwrap()!, setValue)
            } else {
                nullView(setValue)
            }
        }
    }
}

struct DatePickerNullable: View {
    var title: String
    var selected: Binding<Date?>
    @State var defaultToday: Bool = false

    var body: some View {
        NullableBindedValue(
            selected,
            defaultView: { date, setDate in
                let setDateNil = {
                    setDate(nil)
                    self.defaultToday = false
                }

                return AnyView(
                    HStack {
                        DatePicker(
                            "",
                            selection: date,
                            displayedComponents: [.date, .hourAndMinute]
                        ).font(.title2)
                        Button(action: setDateNil) {
                            Image(systemName: "xmark.circle")
                                .foregroundColor(Color.defaultColor)
                                .font(.title2)
                        }
                        .buttonStyle(PlainButtonStyle())
                        .background(Color.clear)
                        .cornerRadius(10)
                    }
                )
            },
            nullView: { setDate in
                let setDateNow = {
                    setDate(Date())
                }

                return AnyView(
                    HStack {
                        TextField(
                            title,
                            text: .constant("Is empty")
                        ).font(.title2).disabled(true).textFieldStyle(RoundedBorderTextFieldStyle())
                        Button(action: setDateNow) {
                            Image(systemName: "plus.circle")
                                .foregroundColor(Color.defaultColor)
                                .font(.title2)
                        }
                        .buttonStyle(PlainButtonStyle())
                        .background(Color.clear)
                        .cornerRadius(10)
                    }.onAppear(perform: {
                        if self.defaultToday {
                            setDateNow()
                        }
                    })
                )
            }
        )
    }
}

评论

0赞 malhal 2/15/2023
我认为最好避免在视图中ifbodyNullableBindedValue
0赞 Sentry.co 11/22/2023
不错的解决方案!如果将 typealias 用于冗长的变量签名,可能会更具可读性吗?
3赞 emehex 12/16/2022 #4

大多数可选的绑定问题都可以通过以下方式解决:

public func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

以下是我如何将其与 DatePicker 一起使用:

DatePicker(
    "",
    selection: $testDate ?? Date(),
    displayedComponents: [.date]
)

评论

0赞 Sentry.co 11/22/2023
不错的绑定黑客!谢谢!
1赞 kittonian 4/11/2023 #5

若要解决此问题,请在 Binding 上创建一个扩展,以自动提供使用可选项的简单方法。将其放置在项目中存在其他全局扩展的位置。

extension Binding {
    func bindUnwrap<T>(defaultVal: T) -> Binding<T> where Value == T? {
        Binding<T>(
            get: {
                self.wrappedValue ?? defaultVal
            }, set: {
                self.wrappedValue = $0
            }
        )
    }
}

然后,要将该扩展与视图中的 DatePicker 一起使用,请执行以下操作:

DatePicker(
    "My Title",
    selection: $myVar.bindUnwrap(defaultVal: Date()),
    in: ...Date(),
    displayedComponents: [.date]
)

希望对您有所帮助!