PickerView 键盘并点击以关闭 SwiftUI

Noa*_*der 5 picker dismiss ios swift swiftui

Xcode 11.2.1、斯威夫特 5.0

我想尝试复制苹果健康应用程序中的这个功能:

选取器视图功能

该功能包括以下几项内容:

  1. PickerView有点像敲击键盘。

  2. PickerView被外面的水龙头解雇。

到目前为止,我已经尝试让与此非常相似的事情启动并运行。例如,我试图获取带有自定义正文(将填充整个屏幕)的警报,您可以从任何工作视图调用该警报,但我发现它具有挑战性,原因有很多,包括但不限于:将视图定位在其自己的本地坐标系中,无法在 SwiftUI 布局系统中的所有其他视图之上添加视图等。选择器的类似键盘的呈现风格使得将其布局在其他一切,与警报类似。关于外部点击关闭的功能,我也尝试过以某种方式实现,但没有成功。执行此操作的理想方法是在除子视图之外的所有内容前面添加某种手势识别器(在我的实验中,我试图将其添加到 a 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))
    }
}

Run Code Online (Sandbox Code Playgroud)

问题:

  • 由于动画无法真正知道视图仅占据屏幕的底部部分,因此它的构建速度太快,因为最终距离更远。

  • 我无法使用设备的安全区域来填充我的视图,因此我最终猜测将其在底部填充 20,但这不是一个可扩展性很强的解决方案。

  • 我不认为这会很好地扩大规模。坦率地说,这感觉有点像一个老套的解决方案,因为我们真的不想在子边界内布局视图,但我们有点被迫这样做。此外,如果调用的子视图不在屏幕上的最高 z-index 位置,您会立即遇到问题,因为其他项目会阻碍它和警报(选择器弹出窗口)。

注意:我确信它的问题比我提到的要多,我只是目前无法想到它们,无论如何,这些是一些主要问题。


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

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

谢谢你!

SJo*_*shi 3

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

对我来说,关键的障碍是在 TapGesture 上使用遮罩,否则我之前使用的 onTapGesture 会吞掉所有表单点击。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这是主要内容视图:

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))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)