简体   繁体   中英

SwiftUI trouble with index out of range

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!

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:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM