SwiftUI 自定义 PickerStyle
Posted
技术标签:
【中文标题】SwiftUI 自定义 PickerStyle【英文标题】:SwiftUI Custom PickerStyle 【发布时间】:2019-11-15 14:24:39 【问题描述】:我正在尝试编写一个与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()
的东西吗?
【问题讨论】:
不是公开的api。但是您可以在不使用这种样式的情况下设计您的所有者选择器 嗯,有道理。所以我必须从头开始构建自己的选择器? 在这里构建一个与使用样式构建相同的东西并不难。你只需要一个模板。真正困难的部分是实现您的设计。 【参考方案1】:我还没有完成它,因为出现了其他东西,但这是我的(未完成的实现 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
如果你弄清楚了这一点并修复了选择器中内容的对齐方式,它基本上是完全可调节的......你也可以通过 width
和 height
使用 GeometryReader
的洞东西
祝你好运!
P.S.:我会在它完成后更新它,但目前它不是我的第一要务,所以不要指望我这样做。
【讨论】:
不错的起点。我正在做类似的事情,我认为明天应该有这个实现。滑动背景应调整为控制元素的大小。我认为可以实现。【参考方案2】:以下代码简化了SegmentPickerElementView
的设计和选择状态的维护。此外,它修复了原始帖子中选择指示器的大小(宽度和高度)计算。请注意,此解决方案中的指标位于前景中,有效地“滑动”在选择(段)HStack
的表面上。最后,这是在 iPad 上使用 Swift Playgrounds 开发的。如果您在 Mac 上使用 XCode,则需要注释掉 PlaygroundSupport
代码,并取消注释 SegmentedPickerView_Previews
结构代码。
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()//.background(Color.white)
//.background(Color.green)
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)
.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" ]))
【讨论】:
以上是关于SwiftUI 自定义 PickerStyle的主要内容,如果未能解决你的问题,请参考以下文章