Array.forEach creates error “Cannot convert value of type '()' to closure result type '_'”

会有一股神秘感。 提交于 2020-02-24 09:31:06

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!