1

This little code is showing articles from an API with 2 columns with scrollview on ArticleView. Unfortunately after the 6th loadMoreContentIfNeeded it gives me hard time with this error

Thread 1: Fatal error: each layout item may only occur once

I have tried adding .id(UUID()) on my code to avoid id problems but it did not help either. I also observed that this error happens in LazyVStavk too. If you change LazyVStack to VStack this problem won't occur. But there is not anything in SwiftUI called VGrid.

struct ArticleView: View {
@ObservedObject var service = ArticleService()
let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 0), count: 2)
var body: some View {     
ScrollView(.vertical, showsIndicators: false) {
                        LazyVGrid(columns: columns, spacing:0) {
                            ForEach(Array(service.articlePost.enumerated()), id: \.1.article_id) { i, post in
                                ArticleListElementView(articlePost: post, cellNumber: i)
                                    .id(UUID())
                                    .onAppear {
                                        self.service.loadMoreContentIfNeeded(currentItem: post)
                                    }
                                    .padding(.all, 20)
                                    .onTapGesture {
                                        self.shown = true
                                        self.selectedArticle = post
                                    }
                               }
                          }
                     }

 private func loadMoreContent() {
    
    guard !isLoadingPage && canLoadMorePages else {
        return
    }
    isLoadingPage = true
    
    let parameters = ["index": "articles", "start" : "\(currentPage)"]
    if let url = URL(string: "http:/v2/search/") {
        let session = URLSession(configuration: .default)
        var request = URLRequest(url:url)
        request.httpMethod = "POST"
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
        } catch let error {
            print(error.localizedDescription)
        }
        
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        
        session.dataTaskPublisher(for: request as URLRequest)
            .map(\.data)
            .decode(type: ArticleResults.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .handleEvents(receiveOutput: { response in
                self.isLoadingPage = false
                self.currentPage += 1
            })
            .map({ response in
                return self.articlePost + response.articles
            })
            .catch({ _ in Just(self.articlePost) })
            .assign(to: &$articlePost)
    }
    
}

func loadMoreContentIfNeeded(currentItem item: ArticlePost?) {
    guard let item = item else {
        loadMoreContent()
        return
    }
    
    let thresholdIndex = articlePost.index(articlePost.endIndex, offsetBy: -5)
    if articlePost.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
        loadMoreContent()
    }
}
Mert Köksal
  • 359
  • 1
  • 9

1 Answers1

3

I can only point you to the issue, because there is still some relevant code missing.

The problem lies in your ForEach or your Array. You are identifying content by id: \.1.article_id. However, I guess! in your array there are items which have the same article id. LazyVGrid requires your content to have an explicit unique id, where VStack doesn't.

As I don't know your Array or loadMoreContentIfNeeded function, I can not point to your issue why there are duplicated items.

Here is an example to show you the difference:

This will crash, because service contains "Test123" twice and you identity it by their content

struct ContentView: View {
    var service = ["Test123", "Test1235", "Test123", "dasdkjasdkas"]
    let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 0), count: 2)
    
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            LazyVGrid(columns: columns, spacing:0) {
                ForEach(service, id: \.self) { element in
                    Text(element)
                        .id(UUID())
                        .onAppear {
                            //self.service.loadMoreContentIfNeeded(currentItem: post)
                        }
                        .padding(.all, 20)
                        .onTapGesture {
                            //self.shown = true
                            //self.selectedArticle = post
                        }
                }
            }
        }
    }
}

Remove one "Test123" and it will work fine.

davidev
  • 5,693
  • 3
  • 7
  • 32
  • I need my .enumarated() part because I am modifying my ArticleListElement according to cell number. So removing this is not an option. – Mert Köksal Dec 16 '20 at 18:05
  • Yes, you can keep that. Make sure that you do not have duplicated items. – davidev Dec 16 '20 at 18:56
  • ok but I am fetching data with articleService() if I make it test123 maybe it will work but it will be useless – Mert Köksal Dec 16 '20 at 20:32
  • 1
    The answer got me on the right path. I simply added `var id = UUID()`to my identifiable struct and removed my previous id creation method. It works now, thanks. – user3191334 Jan 15 '21 at 15:05