SwiftUI自定义选取器样式



我正在尝试编写一个类似于SegmentedPickerStyle()的自定义PickerStyle。这是我目前的状态:

import SwiftUI
public struct FilterPickerStyle: PickerStyle {
public static func _makeView<SelectionValue>(value: _GraphValue<_PickerValue<FilterPickerStyle, SelectionValue>>, inputs: _ViewInputs) -> _ViewOutputs where SelectionValue : Hashable {
}
public static func _makeViewList<SelectionValue>(value: _GraphValue<_PickerValue<FilterPickerStyle, SelectionValue>>, inputs: _ViewListInputs) -> _ViewListOutputs where SelectionValue : Hashable {
}
}

我创建了一个符合PickerStyle协议的结构。Xcode随后添加了所需的协议方法,但我不知道如何使用它们。如果我想实现类似于SegmentedPickerStyle()的东西,有人能解释一下如何处理这些方法吗?

自从出现其他东西以来,我还没有完成它,但这是我(未完成的实现SegmentedPicker的尝试(:


struct SegmentedPickerElementView<Content>: View where Content : View {
@Binding var selectedElement: Int
let content: () -> Content
@inlinable init(_ selectedElement: Binding<Int>, @ViewBuilder content: @escaping () -> Content) {
self._selectedElement = selectedElement
self.content = content
}
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
.contentShape(Rectangle())
}
}
}
struct SegmentedPickerView: View {
@Environment (.colorScheme) var colorScheme: ColorScheme
var elements: [(id: Int, view: AnyView)]
@Binding var selectedElement: Int
@State var internalSelectedElement: Int = 0
private var width: CGFloat = 620
private var height: CGFloat = 200
private var cornerRadius: CGFloat = 20
private var factor: CGFloat = 0.95
private var color = Color(UIColor.systemGray)
private var selectedColor = Color(UIColor.systemGray2)

init(_ selectedElement: Binding<Int>) {
self._selectedElement = selectedElement
self.elements = [
(id: 0, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("4").font(.system(.title))
})),
(id: 1, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("5").font(.system(.title))
})),
(id: 2, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("9").font(.system(.title))
})),
(id: 3, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("13").font(.system(.title))
})),
(id: 4, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("13").font(.system(.title))
})),
(id: 5, view: AnyView(SegmentedPickerElementView(selectedElement) {
Text("13").font(.system(.title))
})),
]
self.internalSelectedElement = selectedElement.wrappedValue
}
func calcXPosition() -> CGFloat {
var pos = CGFloat(-self.width * self.factor / 2.4)
pos += CGFloat(self.internalSelectedElement) * self.width * self.factor / CGFloat(self.elements.count)
return pos
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(self.selectedColor)
.cornerRadius(self.cornerRadius * self.factor)
.frame(width: self.width * self.factor / CGFloat(self.elements.count), height: self.height - self.width * (1 - self.factor))
.offset(x: calcXPosition())
.animation(.easeInOut(duration: 0.2))
HStack(alignment: .center, spacing: 0) {
ForEach(self.elements, id: .id) { item in
item.view
.gesture(TapGesture().onEnded { _ in
print(item.id)
self.selectedElement = item.id
withAnimation {
self.internalSelectedElement = item.id
}
})
}
}
}
.frame(width: self.width, height: self.height)
.background(self.color)
.cornerRadius(self.cornerRadius)
.padding()
}
}
struct SegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
SegmentedPickerView(.constant(1))
}
}

我还没有算出2.4的值所在的公式。。。它取决于元素的数量。。。她就是我学到的:

2个元素=4个

3个元素=3个

4个元素=2.6666

5种元素=约2.4

如果你弄清楚并修复拾取器中内容的对齐方式,它基本上是完全可调的。。。你也可以通过孔的widthheight或使用GeometryReader

祝你好运!

附言:我会在它完成后更新,但目前这不是我的首要任务,所以不要指望我这么做。

以下代码简化了SegmentPickerElementView的设计和选择状态的维护。此外,它还修复了原始过帐中选择指示符的大小(宽度和高度(计算。请注意,此解决方案中的指示符位于前台,有效地在HStack选项(分段(的表面上"滑动"。最后,这是在iPad上使用Swift Playgrounds开发的。如果在Mac上使用XCode,则需要注释掉PlaygroundSupport代码,并取消注释SegmentedPickerView_Previews结构代码。

为iOS 15更新的代码

import Foundation
import Combine
import SwiftUI
import PlaygroundSupport
struct SegmentedPickerElementView<Content>: Identifiable, View where Content : View {
var id: Int
let content: () -> Content

@inlinable init(id: Int, @ViewBuilder content: @escaping () -> Content) {
self.id = id
self.content = content
}

var body: some View {
/*
By simply wrapping “content” in a GeometryReader
you get a view which will flexibly take up the available 
width in the parent container. As "Hacking Swift" put it:
"GeometryReader has an interesting side effect that might 
catch you out at first: the view that gets returned has a 
flexible preferred size, which means it will expand to 
take up more space as needed."
(https://www.hackingwithswift.com/books/ios-swiftui/understanding-frames-and-coordinates-inside-geometryreader)
Interesting side effect, indeed. (Don't know about you, 
but I don't like side effects, interesting or not.) As 
suggested in the cited article, uncomment the 
“background()“ modifiers to see this side effect.
*/
GeometryReader { proxy in
self.content()
// Sizing seems to have changed in iOS 14 or 15
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
}
struct SegmentedPickerView: View {
@Environment (.colorScheme) var colorScheme: ColorScheme
@State var selectedIndex: Int = 0
@State var elementWidth: CGFloat = 0

// The values for width and height are arbitrary, and this part 
// of the implementation can be improved (left to the reader).
private let width: CGFloat = 380
private let height: CGFloat = 72
private let cornerRadius: CGFloat = 8
private let selectorStrokeWidth: CGFloat = 4
private let selectorInset: CGFloat = 6 
private let backgroundColor = Color(UIColor.lightGray)

private let choices: [String]
private var elements: [SegmentedPickerElementView<Text>] = [SegmentedPickerElementView<Text>]()

init(choices: [String]) {
self.choices = choices
for i in choices.indices {
self.elements.append(SegmentedPickerElementView(id: i) {
Text(choices[i]).font(.system(.title))
})
}
self.selectedIndex = 0
}

@State var selectionOffset: CGFloat = 0
func updateSelectionOffset(id: Int) {
let widthOfElement = self.width/CGFloat(self.elements.count)
self.selectedIndex = id
selectionOffset = CGFloat((widthOfElement * CGFloat(id)) + widthOfElement/2.0)
}

var body: some View {
VStack {
ZStack(alignment: .leading) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.elements) { item in
(item as SegmentedPickerElementView )
.onTapGesture(perform: { 
withAnimation {
self.updateSelectionOffset(id: item.id)
}
})
}
}
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Color.gray, lineWidth: selectorStrokeWidth)
.foregroundColor(Color.clear)
// add color highlighting (optional)
.background(.yellow.opacity(0.25)) 
.frame(
width: (width/CGFloat(elements.count)) - 2.0 * selectorInset, 
height: height - 2.0 * selectorInset)
.position(x: selectionOffset, y: height/2.0)
.animation(.easeInOut(duration: 0.2))
}
.frame(width: width, height: height)
.background(backgroundColor)
.cornerRadius(cornerRadius)
.padding()

Text("selected element: (selectedIndex) -> (choices[selectedIndex])")
}.onAppear(perform: { self.updateSelectionOffset(id: 0) })
}
}
//  struct SegmentedPickerView_Previews: PreviewProvider {
//      static var previews: some View {
//          SegmentedPickerView(choices: ["A", "B", "C", "D", "E", "F" ])
//      }
//  }
PlaygroundPage.current.setLiveView(SegmentedPickerView(choices: ["A", "B", "C", "D", "E", "F" ]))

最新更新