Reputation: 606
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!
Upvotes: 1
Views: 1358
Reputation: 54516
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:
Upvotes: 2
Reputation: 299345
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.
Upvotes: 0