1

I know its a really simple question but I'm just stuck on it atm so any advice would be greatly appreciated as I am new to SwiftUI.

I am trying to download text from firebase and render it to the view but I keep getting an out of range error:

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

The code is as follows:

var body: some View{
    
    ZStack {
        
        if fetch.loading == false {
            LoadingView()
        }
        
        else{
            Text(names[0])
                .bold()
        }
    }
    .onAppear {
        self.fetch.longTask()
    }
    
}

Here is the Fetch Content Page:

@Published var loading = false

func longTask() {

    DispatchQueue.main.asyncAfter(deadline: .now()) {
        
        let db = Firestore.firestore()
        db.collection("Flipside").getDocuments { (snapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
                
                return
            } else {
                for document in snapshot!.documents {
                    let name = document.get("Name") as! String
                    let description = document.get("Description") as! String
                    //name = items[doc]
                    print("Names: ", name)
                    print("Descriptions: ", description)
                    names.append(name)
                    descriptions.append(description)
                }
            }
        }
        self.loading = true
    }
}

So basically when the view appears, get the data from Firebase when the data has downloaded display the menuPage() until then show the Loading Data text.

Any help is welcome!

pawello2222
  • 46,897
  • 22
  • 145
  • 209
mick1996
  • 516
  • 9
  • 31
  • This will not solve your issue, but FYI, your `completed()` call will happen before the Firebase call completes, since `getDocuments` is an asynchronous function. You'll need to call `completed` inside the `getDocuments` closure to have it behave like you're looking for. – jnpdx Mar 15 '21 at 21:27
  • @jnpdx thank you very much for your response! I will add that now! have you any idea what else I need to get to get it to work as expected? – mick1996 Mar 15 '21 at 21:35

2 Answers2

2

As Rob Napier mentioned, the issue is that you're accessing the array index before the array is populated.

I'd suggest a couple of improvements to your code. Also, instead of maintaining separate arrays (names, descriptions, ...) you can create a struct to hold all the properties in one place. This will allow you to use just one array for your items.

struct Item {
    let name: String
    let description: String
}
class Fetch: ObservableObject {
    @Published var items: [Item] = [] // a single array to hold your items, empty at the beginning
    @Published var loading = false // indicates whether loading is in progress

    func longTask() {
        loading = true // start fetching, set to true
        let db = Firestore.firestore()
        db.collection("Flipside").getDocuments { snapshot, err in
            if let err = err {
                print("Error getting documents: \(err)")
                DispatchQueue.main.async {
                    self.loading = false // loading finished
                }
            } else {
                let items = snapshot!.documents.map { document in // use `map` to replace `snapshot!.documents` with an array of `Item` objects
                    let name = document.get("Name") as! String
                    let description = document.get("Description") as! String
                    print("Names: ", name)
                    print("Descriptions: ", description)
                    return Item(name: name, description: description)
                }
                DispatchQueue.main.async { // perform assignments on the main thread
                    self.items = items
                    self.loading = false // loading finished
                }
            }
        }
    }
}
struct ContentView: View {
    @StateObject private var fetch = Fetch() // use `@StateObject` in iOS 14+

    var body: some View {
        ZStack {
            if fetch.loading { // when items are being loaded, display `LoadingView`
                LoadingView()
            } else if fetch.items.isEmpty { // if items are loaded empty or there was an error
                Text("No items")
            } else { // items are loaded and there's at least one item
                Text(fetch.items[0].name)
                    .bold()
            }
        }
        .onAppear {
            self.fetch.longTask()
        }
    }
}

Note that accessing arrays by subscript may not be needed. Your code can still fail if there's only one item and you try to access items[1].

Instead you can probably use first to access the first element:

ZStack {
    if fetch.loading {
        LoadingView()
    } else if let item = fetch.items.first {
        Text(item.name)
            .bold()
    } else {
        Text("Items are empty")
    }
}

or use a ForEach to display all the items:

ZStack {
    if fetch.loading {
        LoadingView()
    } else if fetch.items.isEmpty {
        Text("Items are empty")
    } else {
        VStack {
            ForEach(fetch.items, id: \.name) { item in
                Text(item.name)
                    .bold()
            }
        }
    }
}

Also, if possible, avoid force unwrapping optionals. The code snapshot!.documents will terminate your app if snapshot == nil. Many useful solutions are presented in this answer:

pawello2222
  • 46,897
  • 22
  • 145
  • 209
0

The basic issue is that you're evaluating names[0] before the names array has been filled in. If the Array is empty, then you would see this crash. What you likely want is something like:

Item(title: names.first ?? "", ...)

The reason you're evaluating names[0] too soon is that you call completed before the fetch actually completes. You're calling it synchronously with the initial method call.

That said, you always must consider the case where there are connection errors or or the data is empty or the data is corrupt. As a rule, you should avoid subscripting Arrays (preferring things like .first), and when you do subscript Arrays, you must first make sure that you know how many elements there are.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you for you response! I really do appreciate it! How can I make sure that the view isnt rendered and the array isnt filled until the fetch has completed? – mick1996 Mar 15 '21 at 22:23
  • First, move `completed()` to be called only after the fetch completes (it needs to be in the completion handler, after you've set names). Beyond that, it depends on the rest of your code, but if this is everything, then that should be sufficient. – Rob Napier Mar 16 '21 at 13:13
  • Hi there, I have tried a few different ways now and I still cant seem to get it to work. would you be able to tell me what I am doing wrong please? – mick1996 Mar 16 '21 at 21:43
  • We would need to see what you've tried, and what "can't seem to get it to work" means. What is the current code, now that you've moved the `completed()` call and removed the `names[0]`? What is the new error (it is unlikely to be exactly the same). Update your question if there is more information. – Rob Napier Mar 16 '21 at 22:14
  • Hi Rob, thank you for your continued responses. I am new to swiftUI so I'm a bit lost at the moment. As you can see from my updated question I have changed tack a little bit since I couldn't get it to work using the completed method. Am I just missing something simple like a wait() until the firebase function has completed? Thank you for your help! – mick1996 Mar 16 '21 at 22:29
  • I am basing my change of tack on this solution: https://stackoverflow.com/a/63731149/10459255 but am still getting the same error – mick1996 Mar 16 '21 at 22:30
  • Put `print()` statements around your update to `names` and your update of `loading`. You're setting `loading` to true (which causes you to access `names[0]`) before you set `names[0]`. This is the same issue as before. – Rob Napier Mar 16 '21 at 22:39
  • If I don't set loading to true, then it will never display the new view. I'm sorry if what i'm doing is obvious but I just don't see where I can set it to true after it has been completed?? – mick1996 Mar 16 '21 at 22:44
  • 1
    After the `for` loop in the `else` block in the completion handler to `getDocuments`. Add `print()` statements throughout this code and see when it's run. It's not running in the order you think it is. (My suspicion is that you don't understand how completion handlers work, but it's not clear.) – Rob Napier Mar 17 '21 at 13:38