问题
I am trying to loop through an array in SwuftUI to render multiple text strings in different locations. Going through the array manually works, but using forEach-loop produces an error. In the code sample below, I have commented out the manual approach (which works). This kind of approach worked in this tutorial for drawing lines (https://developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes)
(As a bonus question: is there a way to get the index/key of the individual positions through this approach, without adding that key in to the positions-Array for each of the rows?)
I have tried various approaches like ForEach, adding identified() in there as well, and adding various type definitions for the closure, but I just end up creating other errors
import SwiftUI
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
positions.forEach { (position) in
Text("Hello World")
.position(position)
}
/* The above approach produces error, this commented version works
Text("Hello World")
.position(positions[0])
Text("Hello World")
.position(positions[1])
Text("Hello World")
.position(positions[2]) */
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
回答1:
In the Apple tutorial you pointed to, they used the ´.forEach´ inside the Path closure, which is a "normal" closure. SwiftUI, uses a new swift feature called "function builders". The { brackets }
after ZStack might look like a usual closure, but it's not!
See eg. https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api for more about function builders.
In essence, the "function builder" (more specifically, ViewBuilder
, in this case; read more: https://developer.apple.com/documentation/swiftui/viewbuilder) get an array of all the statements in the "closure", or rather, their values. In ZStack, those values are expected to be conforming to the View
protocol.
When you run someArray.forEach {...}
, it will return nothing, void, also known as ()
. But the ViewBuilder expected something conforming to the View
protocol! In other words:
Cannot convert value of type '()' to closure result type '_'
Of course it can't! Then, how might we do a loop/forEach that returns what we want?
Again, looking at the SwiftUI documentation, under "View Layout and Presentation" -> "Lists and Scroll Views", we get: ForEach, which allows us to describe the iteration declaratively, instead of imperatively looping through the positions: https://developer.apple.com/documentation/swiftui/foreach
When a view's state changes, SwiftUI regenerates the struct describing the view, compares it with the old struct, and then only makes the necessary patches to the actual UI, to save performance and allow for fancier animations, etc. To be able to do this, it needs to be able to identify each item in eg. a ForEach (eg. to distinguish an insert of a new point from just a change of an existing one). Thus, we can't just pass the array of CGPoints directly to ForEach (at least not without adding an extension to CGPoint, making them conform to the Identifiable protocol). We could make a wrapper struct:
import SwiftUI
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct Note: Identifiable {
let id: Int
let position: CGPoint
let text: String
}
var notes = positions.enumerate().map { (index, position) in
// using initial index as id during setup
Note(index, position, "Point \(index + 1) at position \(position)")
}
struct ContentView: View {
var body: some View {
ZStack {
ForEach(notes) { note in
Text(note.text)
.position(note.position)
}
}
}
}
We could then add the ability to tap-and-drag the notes. When tapping a note, we might want to move it to the top of the ZStack. If any animation was playing on the note (for instance, changing its position during drag), it would normally stop (because the whole note-view would be replaced), but because the note struct now is Identifiable
, SwiftUI will understand that it's only been moved, and make the change without interfering with any animation.
See https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-views-in-a-loop-using-foreach or https://medium.com/@martinlasek/swiftui-dynamic-list-identifiable-73c56215f9ff for a more in depth tutorial :)
note: the code has not been tested (gah beta Xcode)
回答2:
Not everything is valid in a view body. If you would like to perform a for-each loop, you need to use the special view ForEach
:
https://developer.apple.com/documentation/swiftui/foreach
The view requires an identifiable array, and the identifiable array requires elements to conform to Hashable. Your example would need to be rewritten like this:
import SwiftUI
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
ForEach(positions.identified(by: \.self)) { position in
Text("Hello World")
.position(position)
}
}
}
}
Alternatively, if your array is not identifiable, you can get away with it:
var positions: [CGPoint] = [
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 200),
CGPoint(x: 100, y: 300),
]
struct ContentView : View {
var body: some View {
ZStack {
ForEach(0..<positions.count) { i in
Text("Hello World")
.position(positions[i])
}
}
}
}
来源:https://stackoverflow.com/questions/56916079/array-foreach-creates-error-cannot-convert-value-of-type-to-closure-result