问题
I am trying to create a SwiftUI picker that users can use to select a number from 1000 to 20000 (in increments of 1000. for example 1000,2000,3000 .... ... 20000)
By default the SwiftUI picker can only hold 10 rows of text. How can I allow the SwiftUI picker to contain 20 rows of text?
回答1:
I guess you wrote something like this:
struct ContentView: View {
var body: some View {
Picker(selection: $value, label: Text("Pick One")) {
Text("1000").tag(1000)
Text("2000").tag(2000)
Text("3000").tag(3000)
Text("4000").tag(4000)
Text("5000").tag(5000)
Text("6000").tag(6000)
Text("7000").tag(7000)
Text("8000").tag(8000)
Text("9000").tag(9000)
Text("10000").tag(10000)
}
}
@State var value: Int = 1000
}
And then you tried to add a row for 11000 and got this error:
error: picker.xcplaygroundpage:5:31: error: cannot convert value of type 'Binding<Int>' to expected argument type 'Binding<_>'
Picker(selection: $value, label: Text("Pick One")) {
^~~~~~
The problem is that, due to limitations in the Swift language and in how SwiftUI is implemented, you can only have 10 subviews in a @ViewBuilder
body.
Here are two ways to work around this.
One way, which is appropriate for your design, is to use ForEach
:
struct ContentView: View {
var body: some View {
Picker(selection: $value, label: Text("Pick One")) {
ForEach(Array(stride(from: 1000, through: 20000, by: 1000))) { number in
Text("\(number)").tag(number)
}
}
}
@State var value: Int = 1000
}
The other way, which would be more appropriate if your items didn't follow a simple pattern, is to group your items using Group
:
struct ContentView: View {
var body: some View {
Picker(selection: $value, label: Text("Pick One")) {
Group {
Text("1000").tag(1000)
Text("2000").tag(2000)
Text("3000").tag(3000)
Text("4000").tag(4000)
Text("5000").tag(5000)
Text("6000").tag(6000)
Text("7000").tag(7000)
Text("8000").tag(8000)
Text("9000").tag(9000)
Text("10000").tag(10000)
}
Group {
Text("11000").tag(11000)
Text("12000").tag(12000)
Text("13000").tag(13000)
Text("14000").tag(14000)
Text("15000").tag(15000)
Text("16000").tag(16000)
Text("17000").tag(17000)
Text("18000").tag(18000)
Text("19000").tag(19000)
Text("20000").tag(20000)
}
}
}
@State var value: Int = 1000
}
SwiftUI flattens the Group
subviews into the Group
's parent (in this case, into the Picker
). Each Group
can have up to 10 subviews, which can themselves be Group
s, so by nesting Group
s you can have arbitrarily many explicit elements in your Picker
. But I recommend using ForEach
.
If you want to understand where the 10 subview limit comes from, edit my second example, storing the Picker
in a variable like this:
struct ContentView: View {
var body: some View {
let picker = Picker(selection: $value, label: Text("Pick One")) {
Group {
...
}
}
return picker
}
}
Now option-click the picker
variable in Xcode to see its inferred type:
Let's reformat that more readably:
let picker: Picker<
Text,
Int,
TupleView<(
Group<TupleView<(
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View)>>,
Group<TupleView<(
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View,
some View)>>)>>
Wow, that's a big type! SwiftUI uses generic types like this heavily because it is more efficient at runtime. Because these are all struct
types conforming to View
, Swift stores this whole Picker
and all its children in a single contiguous block of memory. That block can start out on the stack and only needs to be copied to the heap when SwiftUI eventually needs to type-erase it or store it long-term. Compare to UIKit, where every view is always separately allocated on the heap on creation.
ViewBuilder is the SwiftUI utility that assembles these complex views. Swift transforms the body of each Group
into a call to ViewBuilder.buildBlock
, with each view inside the Group
body as a separate argument to ViewBuilder.buildBlock
. Each of those arguments could be a separate type (e.g. a Group
could have some Text
children and some Image
children). But Swift doesn't support variadic generics, so ViewBuilder
has to define a version of buildBlock
that takes a single view, and a version that takes two views, and a version that takes three views, and so on. It cannot define an infinite number of methods, because then the SwiftUI framework would be infinitely large. So it stops at 10 arguments:
static func buildBlock() -> EmptyView Builds an empty view from a block containing no statements. static func buildBlock<Content>(Content) -> Content Passes a single view written as a child view through unmodified. static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)> static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)> static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)> static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)> static func buildBlock<C0, C1, C2, C3, C4, C5>(C0, C1, C2, C3, C4, C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(C0, C1, C2, C3, C4, C5, C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(C0, C1, C2, C3, C4, C5, C6, C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(C0, C1, C2, C3, C4, C5, C6, C7, C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
That's why any view whose content is defined using ViewBuilder
(which includes VStack
, HStack
, ZStack
, Picker
, List
, Group
, and others) can only have 10 direct subviews.
来源:https://stackoverflow.com/questions/57173197/how-can-i-increase-maximum-number-of-rows-that-are-shown-in-a-swiftui-picker