如何以编程方式触发 SwiftUI DatePicker?

问题描述 投票:0回答:2

如下图所示,如果您输入日期“2023 年 1 月 11 日”,它会显示日期选择器。我想要实现的是在其他地方有一个按钮,单击该按钮时,自动显示此日期选择器。

有谁知道有没有办法实现?

DatePicker("Jump to", selection: $date, in: dateRange, displayedComponents: [.date]) 

下面是对@rob mayoff 答案的测试。我还是不能 找出为什么它还不起作用。

我使用 iPhone 14 和 iOS 16.2 模拟器在 Xcode 14.2 以及设备上进行了测试。我注意到,虽然调用了

triggerDatePickerPopover()
,但它永远无法到达
button.accessibilityActivate()

import SwiftUI

struct ContentView: View {
  @State var date: Date = .now
  let dateRange: ClosedRange<Date> = Date(timeIntervalSinceNow: -864000) ... Date(timeIntervalSinceNow: 864000)

  var pickerId: String { "picker" }

  var body: some View {
    VStack {
      DatePicker(
        "Jump to",
        selection: $date,
        in: dateRange,
        displayedComponents: [.date]
      )
      .accessibilityIdentifier(pickerId)

      Button("Clicky") {
        triggerDatePickerPopover()
          print("Clicky Triggered")
      }
    }
    .padding()
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

extension NSObject {
  func accessibilityDescendant(passing test: (Any) -> Bool) -> Any? {

    if test(self) { return self }

    for child in accessibilityElements ?? [] {
      if test(child) { return child }
      if let child = child as? NSObject, let answer = child.accessibilityDescendant(passing: test) {
        return answer
      }
    }

    for subview in (self as? UIView)?.subviews ?? [] {
      if test(subview) { return subview }
      if let answer = subview.accessibilityDescendant(passing: test) {
        return answer
      }
    }

    return nil
  }
}


extension NSObject {
  func accessibilityDescendant(identifiedAs id: String) -> Any? {
    return accessibilityDescendant {
      // For reasons unknown, I cannot cast a UIView to a UIAccessibilityIdentification at runtime.
      return ($0 as? UIView)?.accessibilityIdentifier == id
      || ($0 as? UIAccessibilityIdentification)?.accessibilityIdentifier == id
    }
  }
    
    func buttonAccessibilityDescendant() -> Any? {
       return accessibilityDescendant { ($0 as? NSObject)?.accessibilityTraits == .button }
     }
}

extension ContentView {
  func triggerDatePickerPopover() {
    if
      let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
      let window = scene.windows.first,
      let picker = window.accessibilityDescendant(identifiedAs: pickerId) as? NSObject,
      let button = picker.buttonAccessibilityDescendant() as? NSObject
    {
        print("triggerDatePickerPopover")
      button.accessibilityActivate()
    }
  }
}

更新2: 我按照调试说明进行操作。似乎使用完全相同的代码。我的检查员缺少可访问性标识符。不知道为什么……现在感觉心烦意乱。

这里是下载项目的链接https://www.icloud.com/iclouddrive/040jHC0jwwJg3xgAEvGZShqFg#DatePickerTest

更新3: @rob Mayoff 的解决方案非常棒!对于任何阅读的人。如果它对您的情况不起作用,请等待。这可能只是由于设备或模拟器准备好可访问性。

ios swift date swiftui datepicker
2个回答
7
投票

更新(2)

回想起来,我最初使用辅助功能 API 的解决方案有点冒险,因为 Apple 将来可能会更改

DatePicker
的辅助功能结构。我应该先仔细看看
DatePicker
文档,并注意到
.datePickerStyle(.graphical)
修饰符,它可以让您直接使用弹出窗口中显示的日历视图。

由于 SwiftUI 不支持 iPhone 上的自定义弹出窗口(

.popover
修饰符显示一个工作表),一个相当简单的解决方案是将图形
DatePicker
直接放入您的
VStack
中,并用任意数字显示/隐藏它按钮。您可以设置按钮的样式,使其看起来像默认的
DatePicker
按钮。

这是一个完整的示例。看起来像这样:

这是代码:

struct ContentView: View {
  @State var date = Date.now
  @State var isShowingDatePicker = false

  let dateRange: ClosedRange<Date> = Date(timeIntervalSinceNow: -864000) ... Date(timeIntervalSinceNow: 864000)

  private var datePickerId: String { "picker" }

  var body: some View {
    ScrollViewReader { reader in
      ScrollView(.vertical) {
        VStack {
          VStack {
            HStack {
              Text("Jump to")
              Spacer()
              Button(
                action: { toggleDatePicker(reader: reader) },
                label: { Text(date, format: Date.FormatStyle.init(date: .abbreviated)) }
              )
              .buttonStyle(.bordered)
              .foregroundColor(isShowingDatePicker ? .accentColor : .primary)
            }

            VStack {
              DatePicker("Jump to", selection: $date, in: dateRange, displayedComponents: .date)
                .datePickerStyle(.graphical)
                .frame(height: isShowingDatePicker ? nil : 0, alignment: .top)
                .clipped()
                .background {
                  RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .foregroundColor(Color(UIColor.systemBackground))
                    .shadow(radius: 1)
                }
            }
            .padding(.horizontal, 8)
          }.id(datePickerId)

          filler

          Button("Clicky") { toggleDatePicker(true, reader: reader) }
        }
      }
    }
    .padding()
  }

  private func toggleDatePicker(_ show: Bool? = nil, reader: ScrollViewProxy) {
    withAnimation(.easeOut) {
      isShowingDatePicker = show ?? !isShowingDatePicker
      if isShowingDatePicker {
        reader.scrollTo(datePickerId)
      }
    }
  }

  private var filler: some View {
    HStack {
      Text(verbatim: "Some large amount of content to make the ScrollView useful.\n\n" + Array(repeating: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", count: 4).joined(separator: "\n\n"))
        .lineLimit(nil)
      Spacer()
    }
  }
}

原版

SwiftUI 不提供以编程方式触发日历弹出窗口的直接方法。

但是,我们可以使用辅助功能 API 来做到这一点。这是我的测试的样子:

您可以看到通过单击“Clicky”按钮或日期选择器本身打开了日历弹出窗口。

首先,我们需要一种使用辅助功能 API 来查找选择器的方法。让我们为选择器分配一个辅助功能标识符:

struct ContentView: View {
  @State var date: Date = .now
  let dateRange: ClosedRange<Date> = Date(timeIntervalSinceNow: -864000) ... Date(timeIntervalSinceNow: 864000)

  var pickerId: String { "picker" }

  var body: some View {
    VStack {
      DatePicker(
        "Jump to",
        selection: $date,
        in: dateRange,
        displayedComponents: [.date]
      )
      .accessibilityIdentifier(pickerId)

      Button("Clicky") {
        triggerDatePickerPopover()
      }
    }
    .padding()
  }
}

在编写

triggerDatePickerPopover
之前,我们需要一个搜索可访问性元素树的函数:

extension NSObject {
  func accessibilityDescendant(passing test: (Any) -> Bool) -> Any? {

    if test(self) { return self }

    for child in accessibilityElements ?? [] {
      if test(child) { return child }
      if let child = child as? NSObject, let answer = child.accessibilityDescendant(passing: test) {
        return answer
      }
    }

    for subview in (self as? UIView)?.subviews ?? [] {
      if test(subview) { return subview }
      if let answer = subview.accessibilityDescendant(passing: test) {
        return answer
      }
    }

    return nil
  }
}

让我们用它来编写一个方法来搜索具有特定 id 的元素:

extension NSObject {
  func accessibilityDescendant(identifiedAs id: String) -> Any? {
    return accessibilityDescendant {
      // For reasons unknown, I cannot cast a UIView to a UIAccessibilityIdentification at runtime.
      return ($0 as? UIView)?.accessibilityIdentifier == id
      || ($0 as? UIAccessibilityIdentification)?.accessibilityIdentifier == id
    }
  }
}

我在测试中发现,尽管

UIView
被记录为符合
UIAccessibilityIdentification
协议(定义了
accessibilityIdentifier
属性),但将
UIView
转换为
UIAccessibilityIdentification
在运行时不起作用。所以上面的方法比你想象的要复杂一些。

事实证明,选择器有一个充当按钮的子元素,而该按钮就是我们需要激活的按钮。因此,让我们编写一个也搜索按钮元素的方法:

  func buttonAccessibilityDescendant() -> Any? {
    return accessibilityDescendant { ($0 as? NSObject)?.accessibilityTraits == .button }
  }

最后我们可以编写

triggerDatePickerPopover
方法:

extension ContentView {
  func triggerDatePickerPopover() {
    if
      let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
      let window = scene.windows.first,
      let picker = window.accessibilityDescendant(identifiedAs: pickerId) as? NSObject,
      let button = picker.buttonAccessibilityDescendant() as? NSObject
    {
      button.accessibilityActivate()
    }
  }
}

更新

您说我的代码有问题。由于它对我有用,因此很难诊断问题。尝试启动辅助功能检查器(从 Xcode 的菜单栏中选择 Xcode > 打开开发人员工具 > 辅助功能检查器)。告诉它以模拟器为目标,然后使用直角括号按钮逐步浏览辅助功能元素,直到到达 DatePicker。然后将鼠标悬停在检查器的层次结构窗格中的行上,然后单击圆圈内的右箭头。它应该看起来像这样:

请注意,检查器会看到代码中设置的日期选择器的标识符“picker”,并且该选择器在层次结构中有一个按钮子级。如果您的看起来不同,您需要找出原因并更改代码以匹配。

逐步执行

accessibilityDescendant
方法并手动抛弃孩子(例如
po accessibilityElements
po (self as? UIView)?.subviews
)也可能有所帮助。


0
投票

在 UI 顶部覆盖透明的
DatePicker

您可以在自定义 UI 顶部

.overlay
透明的
DatePicker
。用户认为他们正在点击您的按钮,但实际上他们点击的是“隐形”
DatePicker

@State var date = Date()

Text("Add date: \(date)")
  .overlay {
    DatePicker(
      selection: $date,
      displayedComponents: .date
    ) {}
      .fixedSize()
      .opacity(0.011) // Minimum that still allows this to be tappable
  }

这是它的样子

堆叠
DatePicker

一个问题是

DatePicker
点击区域可能太小。您可以将它们堆叠起来,这样对于最终用户来说,这一切看起来就像一个按钮。

Text("Add date: \(date)")
  .overlay(alignment: .leading) {
    HStack(spacing: 0) {
      ForEach(0..<3) { _ in
        DatePicker(
          selection: $date,
          displayedComponents: .date
        ) {}
          .fixedSize()
          .opacity(0.011) // Minimum that still allows this to be tappable
      }
    }
  }

这就是它的样子

一旦将其设置为完全透明,用户将根本看不到它

© www.soinside.com 2019 - 2024. All rights reserved.