0

I have two nested ForEachs inside a LazyHStack

LazyHStack {
    ForEach(items) { item in
        ForEach(item.urls, id: \.self) {
            Text($0.absoluteString)
        }
    }
}

This snippets compiles, but it immediately crashes with the following error

Fatal error: each layout item may only occur once: file SwiftUI, line 0

I read online that this might be due to ForEach not distinguishing correctly the elements of the collection, even if there're all identifiable. In my case, the items in the outer ForEach are all identifiable, while the inner ForEach is looping through an array of optional URL? objects. I tried to make an URL identifiable using its absolute string (that should be unique, I think), but it did not work.

extension URL: Identifiable {
    var id: String? { absoluteString } 
}

I should add that the same code snippet works fine with a standard HStack. How can I solve this problem?

Dree
  • 702
  • 9
  • 29
  • You might have more than one of each `absoluteString`. – aheze Apr 17 '21 at 15:03
  • Try just `var id: UUID { UUID() }` – aheze Apr 17 '21 at 15:03
  • 1
    I don't count this as a problem, rather the app being smart on runtime... What do you want to achieve? Without some kind of stack in the middle of the two for each s, it make sense that the app crashes because it doesnt know how to layout the second for each well – Mahdi BM Apr 17 '21 at 15:17
  • In addition to @MahdiBM 's comment, you also identified the `urls` with `\.self`, instead of `\.id` or leaving the `id` parameter empty. – George Apr 17 '21 at 15:27

2 Answers2

1

It's up to you exactly how the UI is laid out, but this answer creates a horizontally-scrolling list of URLs which are vertically stacked as part of each item.

Working code:

ScrollView(.horizontal) {
    LazyHStack {
        ForEach(items) { item in
            VStack {
                ForEach(item.urls) {
                    Text($0.absoluteString)
                }
            }
        }
    }
}

Changes:

  1. Wrapped LazyHStack in a ScrollView, so the items off screen can be scrolled to be seen.
  2. Inserted a VStack in between the ForEachs, to determine the layout for each item.
  3. Removed id: \.self from the second ForEach, because you created a custom id already for the URL. Either use id: \.id or don't include the id parameter in the ForEach.

Result:

Result

Another possibility would be to set a unique ID for every element. Basically, if multiple URLs are the same (therefore have the same ID), then the LazyHStack has an issue that the IDs aren't all unique. Link to similar answer here. This is the alternate fix which wouldn't require the VStack in between:

Text($0.absoluteString)
    .id(item.id.uuidString + ($0.id ?? ""))

Edit to support optional URLs

Structure of the data (only difference here is URL was replaced with URLItem so we can hold an optional value):

struct Item: Identifiable {
    let id = UUID()
    let urls: [URLItem]
}

struct URLItem: Identifiable {
    let id = UUID()
    let url: URL?
    
    init(_ url: URL?) {
        self.url = url
    }
}

New example data:

let items: [Item] = [
    Item(urls: [
        URLItem(URL(string: "https://www.google.com")), URLItem(URL(string: "https://www.stackoverflow.com"))
    ]),
    Item(urls: [
        URLItem(URL(string: "https://www.stackoverflow.com")), URLItem(URL(string: ""))
    ])
]

This means that we can now have Identifiable optional URLs. The code should now look like this:

ForEach(item.urls) {
    if let url = $0.url {
        Text(url.absoluteString)
            .id($0.id)
    } else {
        Text("Bad URL")
            .id($0.id)
    }
}

You can handle your own cases now where $0.url is nil.

George
  • 25,988
  • 10
  • 79
  • 133
  • Thank you for this answer. A nested stack is a good solution for my use case, but I still have problems because the `URL` array contains optional values. In this case the compiler returns this error: *Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'URL?' conform to 'Identifiable'*. – Dree Apr 17 '21 at 16:49
  • 1
    @Dree What would you like to do with the optional URLs? Would you like to not display them, display some alternate text such as "No URL", or another way? Can you instead guarantee that these URLs are not nil? For example, if you just don't want to display `nil` URLs, you could have `.compactMap { $0 }` after `item.urls` to remove `nil` values. If there is a problem with the `Text`'s ID, you can just do `.id(UUID())` instead. – George Apr 17 '21 at 17:02
  • Well, inside the `ForEach` there isn't just a simple `Text` view, but instead another view which is in charge to handle the *nil* case for the URLs. Unfortunately I cannot just drop *nil* values using `compactMap`. – Dree Apr 17 '21 at 17:38
  • @Dree Code is now updated to support optional URLs. – George Apr 17 '21 at 18:17
-1

It is working, you had some mistake in code:

for example you should use self in your extension

also you used 2 ForEach inside together for no reason, I would say bad coding, I am not going advice to use 2 ForEach inside together, it bring down the system and your app, use 1 ForEach! However here is your answer:


enter image description here


 struct ContentView: View {
    
    @State private var items: [[URL]] = [[URL(string: "www.apple.com")!, URL(string: "www.amazon.com")!, URL(string: "www.google.com")!], [URL(string: "www.Tesla.com")!]]

    var body: some View {

        LazyHStack {
            
            ForEach(items, id: \.self) { item in
                
                ForEach(item) { url in
                    Text(url.absoluteString)
                }
                
            }
            
        }
        
        
    }
    
}

extension URL: Identifiable {
    public var id: String { self.absoluteString }
}
ios coder
  • 1
  • 4
  • 31
  • 91