1

TL;DR: How does one modify content inside a ForEach structure?

The following is a self-contained Playground, in which the call to frame() is OK in a plain body method, but is a syntax error when wrapped in a ZStack/ForEach loop.

import UIKit
import SwiftUI

struct Item: Identifiable {
    var id = UUID()
    var name: String
    init(_ name:String) { self.name = name }
}

let items = [
    Item("a"), Item("b"), Item("c")
]

struct ContentView: View {    
    var body: some View {
        return Image("imageName")
            .resizable()
            .frame(width:0, height:0)          // This compiles.
    }

   var body2: some View {
        ZStack {
            ForEach(items) { item -> Image in   // Return type required...
            let i = item                        // ...because of this line.
            return Image(i.name)
                    .resizable()                // Parens required.
                    .frame(width: 0, height: 0) // Compile error.
            }
        }
    }
}

Note the line let i = item. It is standing in for code in my app that performs some calculations. The fact that the ForEach closure is not a single expression caused the compiler to complain

Unable to infer complex closure return type; add explicit type to disambiguate.

which motivated my adding the return type. But that brings about the topic of this question, the compiler error:

Cannot convert return expression of type 'some View' to return type 'Image'

It appears that the return value of the frame() call is not an Image but a ModifiedContent<SwiftUI.Image, SwiftUI._FrameLayout>.

I have discovered (thanks to commenters!) I can get around this by (somehow) reducing the ForEach closure to a single expression, which renders inferrable the return type, which I can then remove. Is this my only recourse?

Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55

3 Answers3

1

As far as I can tell, this may just be a limitation of Swift, analogous to or part of this type-inference issue: Why can't the Swift compiler infer this closure's type?

My workaround has been to add functionality to the Item struct. Now every calculation needed inside the ForEach closure is provided by the item instance, in-line to the Image initializer, like this:

var body3: some View {
        ZStack {
            ForEach(items) { item in        // No return type specified.

            // let (width, height) = ...    // Remove pre-calculations that
                                            // confused the compiler.
            Image(item.name)
                .resizable()
                .frame(
                    width : item.width,     // All needed data are
                    height: item.height     // provided in closure param.
                )
            }
        }
    }
}

I will grant that this is more idiomatically functional, though I prefer the clarity of a few well-chosen assignments preceding the call. (If the calculations for the assignments are side-effect-free, then it is essentially SSA-style, which should pass the FP smell test.)

So I call this an “answer”, even a “solution”, although I can still whinge about the unhelpful error messages. But that is a known issue, and smarter people than me are already on that.

Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55
0

Now the mesh, I'll have to think about. But getting that first error to go away requires conforming Item with identifiable.

struct Item: Identifiable {
  var id = UUID()
  var name: String
  var size: CGSize = .zero
}

I also had to write a custom modifier for using the item to create a frame. We'll likely want to use CGRect for the mesh creation, have some way to mess with the origin of the image.

extension View {
  func frame(with size: CGSize) -> some View {
    frame(width: size.width, height: size.height)
  }
}

Then your body will look something like this.

var body: some View {
  ZStack {
    ForEach(items) { item in
      Image(item.name).resizable().frame(with: item.size)
    }
  }
}
jnblanchard
  • 1,182
  • 12
  • 12
  • Items are already Identifiable. If they are not, you get a completely different compile error three lines earlier. I forgot to mention the View extension I was using, similar to yours; I updated the question. I left out the origin-shifting to simplify the question. Removing the `-> Image` from the closure's signature (as you did) brings up another error: "Unable to infer complex closure return type", even *without* the troublesome call to `frame`. Also I notice you omitted the parens from your call to `resizable`. In short, I don't think your answer will compile. – Andrew Duncan Jan 31 '20 at 02:58
  • I duplicated my problem in a Playground and edited the question to provide the code. Removed the irrelevant extension. I can confirm that 1) parens are required in the call to `resizable`, and that 2) the compile error persists. However, see my response to @Asperi. – Andrew Duncan Jan 31 '20 at 17:21
  • Your code and mine differ in the closure's return type, and your version (omitting the return type) appeases the compiler *in the Playground* but not my original code. So you're vindicated. And this should be a good clue. – Andrew Duncan Jan 31 '20 at 17:31
  • I think to fix the inference of the item type, you'll just need to add explicit typing to your Item array. let items: [Item] = [....] – jnblanchard Jan 31 '20 at 17:33
  • See further comments to @Asperi. Thanks for motivating me to use a Playground. – Andrew Duncan Jan 31 '20 at 18:03
0

This is here

ForEach(items) { item -> Image in

you explicitly specify that closure returns Image, but frame returns some View, so type mismatch and compiler error.

Use just as below and it will work

ForEach(items) { item in
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • I understand the type mismatch. If I take your suggestion and remove the return type *in my actual code* I get the error "Unable to infer complex closure return type; add explicit type to disambiguate". But... your suggestion *does* stop the error in the (simplified) Playground example. So now I have to figure out what's different. – Andrew Duncan Jan 31 '20 at 17:26
  • I have re-re-edited the question to focus on what I have found: the compiler's problem in inferring return types when there is more than a single expression. So there is a workaround: rewrite the code to avoid multi-expression bodies. Maybe that is sufficient. – Andrew Duncan Jan 31 '20 at 18:02