PickerView键盘和点击以关闭SwiftUI

时间:2019-12-11 02:13:37

标签: ios swift swiftui picker dismiss

Xcode 11.2.1,Swift 5.0

我想尝试并复制Apple在其健康应用程序中拥有的功能:

PickerView Feature

该功能包括以下几点:

  1. PickerView的显示就像是轻按键盘一样。

  2. PickerView在外部点击时被关闭。

到目前为止,我已经尝试使与此非常相似的事情开始运行。例如,我试图通过自定义主体(将覆盖整个屏幕)来获取警报,您可以从任何视图中调用该主体,但是由于多种原因,我发现它具有挑战性,包括但不限于:受限制只能将视图放置在自己的局部坐标系中,而不能在SwiftUI的布局系统等所有其他视图之上添加视图。选择器的类似键盘的呈现样式很难将其放置在其他一切,类似于警报。关于在外部轻击的功能,我也尝试过以某种方式实施,但没有成功。进行此操作的理想方法是在除子视图之外的所有事物之前添加各种手势识别器(在我对此的实验中,我试图为TextField添加它)。如前所述,这被证明是一项艰巨的任务,因为无论您要放置的是哪个孩子,在全局坐标系中进行布局都是非常棘手的。我认为也许可以通过某种方式在子视图中浏览视图层次结构来解决问题,但是我不确定如何执行此操作(可能不确定首选项键或环境变量),如果这样做完全可以使用,并且不知道在SwiftUI中是否确实存在符合该规范的有意义的内容。我怀疑与UIKit的接口可能是唯一的解决方案,但是我不确定如何为此功能正确执行此操作。

我目前的尝试


struct ContentView: View {
    @State var present = false
    @State var date = Date()

    var formattedDate: String {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter.string(from: date)
    }

    var body: some View {
        VStack {

            HStack {
                Rectangle()
                    .fill(Color.green)
                    .cornerRadius(15)
                    .frame(width: 100, height: 100)

                Rectangle()
                    .fill(Color.red)
                    .cornerRadius(15)
                    .frame(height: 100)
                    .overlay(Text(formattedDate).foregroundColor(.white))
                    .shadow(radius: 10)
                    .onTapGesture { self.present.toggle() }
                    .padding(20)
                    .alert(isPresented: $present, contents: {

                        VStack {
                            Spacer()
                            HStack {
                                Spacer()
                                DatePicker(selection: self.$date, displayedComponents: .date) { EmptyView() }
                                    .labelsHidden()
                                Spacer()
                            }
                            .padding(.bottom, 20)
                            .background(BlurView(style: .light).blendMode(.plusLighter))

                        }
                    }) {
                        withAnimation(.easeInOut) {
                            self.present = false
                        }
                }
            }
        }

    }
}

struct BlurView: UIViewRepresentable {

    typealias UIViewType = UIVisualEffectView

    let style: UIBlurEffect.Style

    func makeUIView(context: Context) -> UIViewType {
        let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: style))
        return visualEffectView
    }

    func updateUIView(_ uiView: UIViewType, context: Context) { }
}

extension View {

func alert<Contents: View>(isPresented: Binding<Bool>, contents: @escaping () -> Contents, onBackgroundTapped: @escaping () -> Void = { }) -> some View {

    self.modifier(AlertView(isPresented: isPresented, contents: contents, onBackgroundTapped: onBackgroundTapped))
    }
}


struct AlertView<Contents: View>: ViewModifier {
    @Binding var isPresented: Bool
    var contents: () -> Contents
    var onBackgroundTapped: () -> Void = { }

    @State private var rect: CGRect = .zero


    func body(content: Content) -> some View {
        if rect == .zero {
            return AnyView(
                content.bindingFrame(to: $rect))
        } else {
            return AnyView(content
                .frame(width: rect.width, height: rect.height)
                .overlay(
                    ZStack(alignment: .center) {
                        Color
                            .black
                            .opacity(isPresented ? 0.7 : 0)
                            .edgesIgnoringSafeArea(.all)
                            .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
                            .position(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, in: .global)
                            .onTapGesture {
                                self.onBackgroundTapped()
                            }
                            .animation(.easeInOut)


                        contents()
                            .position(x: UIScreen.main.bounds.midX, y: isPresented ? UIScreen.main.bounds.midY : (UIScreen.main.bounds.midY + UIScreen.main.bounds.height), in: .global)
                            .animation(.spring())
                    }
                    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height),
                    alignment: .center))
        }

    }
}

public extension View {
    func position(_ position: CGPoint, in coordinateSpace: CoordinateSpace) -> some View {
        return self.position(x: position.x, y: position.y, in: coordinateSpace)
    }
    func position(x: CGFloat, y: CGFloat, in coordinateSpace: CoordinateSpace) -> some View {
        GeometryReader { geometry in
            self.position(x: x - geometry.frame(in: coordinateSpace).origin.x, y: y - geometry.frame(in: coordinateSpace).origin.y)
        }
    }
}

struct GeometryGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        return GeometryReader { geometry in
            self.makeView(geometry: geometry)
        }
    }

    func makeView(geometry: GeometryProxy) -> some View {
        DispatchQueue.main.async {
            self.rect = geometry.frame(in: .global)
        }

        return Rectangle().fill(Color.clear)
    }
}

extension View {
    func bindingFrame(to binding: Binding<CGRect>) -> some View {
        self.background(GeometryGetter(rect: binding))
    }
}

问题:

  • 由于动画无法真正知道视图仅占据屏幕的底部,因此由于最终距离较远,因此使其构建的速度过快。

  • 我无法使用设备的安全区域来填充我的视图,所以最终我只能猜测是将其底部填充20,但这不是一个非常可扩展的解决方案。

  • 我不认为这会很好地扩展。坦率地说,这似乎有点棘手,因为我们并不是真的想在孩子的视野内布局视图,但是我们被迫这样做。此外,如果正在调用的子视图不在屏幕上的最高z索引位置,则会立即遇到问题,因为其他项目会阻碍它和警报(选择器弹出窗口)。

    < / li>

注意:我敢肯定,现在有更多的问题比我刚才提到的要多,但无论如何,这些都是一些主要问题。


如何在SwiftUI中有效复制这些功能?我希望能够从子视图中激活它(就像在单独文件中的子视图中一样,而不是在根视图中)。

注意:与UIKit的接口是完全可以接受的解决方案,只要它只是实现细节,并且在大多数情况下都可以充当常规SwiftUI视图。

谢谢!

1 个答案:

答案 0 :(得分:0)

我正在尝试做类似的事情。这是一个半成品的解决方案(没有使用Geometry Reader来推动屏幕,并且不使用BlurView)。

对我来说,关键的绊脚石是使用TapGesture上的蒙版,否则我以前使用过的onTapGesture吞噬了所有Form丝锥。

struct BottomSheetPicker: View {
    @Binding var selection: Double

    var min: Double = 0.0
    let max: Double
    var by: Double = 1.0
    var format: String = "%d"

    var body: some View {
        Group {
            Picker("", selection: $selection) {
                ForEach(Array(stride(from: min, through: max, by: by)), id: \.self) { item in
                    Text(String(format: self.format, item))
                }
            }
            .pickerStyle(WheelPickerStyle())
            .foregroundColor(.white)
            .frame(height: 180)
            .padding()
        }.background(Color.gray)
    }
}

这是主要的内容视图:

struct MyContentView: View {
    @State private var heightCm = 0.0
    @State private var weightKg = 0.0

    @State private var pickerType = 0 // TODO: Update to Enum
    private var isShowingOverlay: Bool {
        get {
            return self.pickerType != 0
        }
    }

    private let heightFormat = "%.0f cm"
    private let weightFormat = "%.1f kg"

    private var heightPicker: some View {
        BottomSheetPicker(selection: $heightCm, max: 300, format: heightFormat)
    }

    private var weightPicker: some View {
        BottomSheetPicker(selection: $weightKg, max: 200, by: 0.5, format: weightFormat)
    }

    // There are a few nicer ways to do this, just wanted to whip up a quick demo
    private var pickerOverlay: some View {
        Group {
            if pickerType == 1 {
                heightPicker
            } else if pickerType == 2 {
                weightPicker
            } else {
                EmptyView()
            }
        }
    }

    var body: some View {
        ZStack {
            Form {
                Section(header: Text("Personal Information")) {
                    HStack {
                        Text("Height")
                        Spacer()
                        Text(String(format: heightFormat, self.heightCm))
                    }
                    .contentShape(Rectangle())
                    .onTapGesture {
                        self.pickerType = 1
                    }

                    HStack {
                        Text("Weight")
                        Spacer()
                        Text(String(format: weightFormat, self.weightKg))
                    }
                    .contentShape(Rectangle())
                    .onTapGesture {
                        self.pickerType = 2
                    }
                }
            }
            .gesture(TapGesture().onEnded({
                self.pickerType = 0
            }), including: isShowingOverlay ? .all : .none)

            if isShowingOverlay {
                pickerOverlay
                    .frame(alignment: .bottom)
                    .transition(AnyTransition.move(edge: .bottom))
                    .animation(.easeInOut(duration: 0.5))
            }
        }
    }
}