Crash on nested ForEach inside a LazyHStack

596 views Asked by At

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?

2

There are 2 answers

0
ios coder On

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 }
}
4
George On

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.