How to Catch "Index Out of Range" in Swift

How do I catch Index out of range in Swift?

As suggested in comments and other answers it is better to avoid this kind of situations. However, in some cases you might want to check if an item exists in an array and if it does safely return it. For this you can use the below Array extension for safely returning an array item.

Swift 5

extension Collection where Indices.Iterator.Element == Index {
subscript (safe index: Index) -> Iterator.Element? {
return indices.contains(index) ? self[index] : nil
}
}

Swift 4

extension Collection where Indices.Iterator.Element == Index {
subscript (safe index: Index) -> Iterator.Element? {
return indices.contains(index) ? self[index] : nil
}
}

Swift 3

extension Collection where Indices.Iterator.Element == Index {
subscript (safe index: Index) -> Generator.Element? {
return indices.contains(index) ? self[index] : nil
}
}

Swift 2

extension Array {
subscript (safe index: Int) -> Element? {
return indices ~= index ? self[index] : nil
}
}
  • This way you'll never hit Index out of range
  • You'll have to check if the item is nil

refer this question for more



Trying the Swift3 code in a Playground in Xcode 8.3.2 still leads to a
"crash" when I do let ar = [1,3,4], then let v = ar[5]. Why? – Thomas
Tempelmann May 17 at 17:40

You have to use our customized subscript so instead of let v = ar[5], it wll be let v = ar[safe: 5].

Default getting value from array.

let boo = foo[index]

Add use the customized subscript.

let boo = fee[safe: index]

// And we can warp the result using guard to keep the code going without throwing the exception.
guard let boo = foo[safe: index] else {
return
}

Provide a catch if index is out of range (Swift 5)

You can use indices to check if an array contains an index prior to accessing it:

guard myArray.indices.contains(1) else { return }
let value = myArray[1]

Apple documentation: indices

Swift: Using try? to catch Index out of range

In Swift. you can only catch instances of Error that are thrown by code, you can't catch runtime exceptions such as an array bounds violation

You can create your own safeAccess function. You don't say what type is in your array so I will use SomeType as an example

func safeAccess(row:Int, col:Int) -> SomeType? {
guard row >= 0, row < tiles.count else {
return nil
}

guard col >= 0, col < tiles[row].count else {
return nil
}

return tiles[row][col]
}

func isValidPlacement(row:Int,col:Int) -> Bool {

if let t = tiles.safeAccess(row:row-1,col:col), t.isSource {
return true
}

if let t = tiles.safeAccess(row:row,col:col-1),, t.isSource {
return true
}

if let t = tiles.safeAccess(row:row+1,col:col), t.isSource {
return true
}

if let t = tiles.safeAccess(row:row,col:col+1), t.isSource {
return true
}

return false

}

You could also define an extension on Array

extension Array {
func element(at index: Int) -> Element? {
if index >= 0 && index < self.count {
return self[index]
}
return nil
}
}

And to use it:

func isValidPlacement(row:Int,col:Int) -> Bool {
if let tiles = tiles.element(at:row-1), let t = tiles.element(at:col), t.isSource {
return true
}
else if tiles.element(at:row), let t = tiles.element(at:col-1), t.isSource {
return true
}
else if let tiles = tiles.element(at:row+1), let t = tiles.element(at:col), t.isSource {
return true
}
else if let tiles = tiles.element(at:row), let t = tiles.element(at:col+1), t.isSource {
return true
}
else {
return false
}
}

How to solve Fatal Error: Index out of range error in swift?

You just need to iterate your indices in reverse order when removing items in your collection.

for value in headers.indices.reversed() {
if headers[value] == "" {
headers.remove(at: value)
}
}

Note also there is a mutating method that accepts a predicate called removeAll(where:):

headers.removeAll(where: \.isEmpty)

Array index is out of range: when get a range from an array

Besides the fact that you were using the wrong operator beware that not all collections starts at zero and not all collections have all elements up to its index before the endIndex. Thats the case of the result of your subscription which returns an ArraySlice. You should always check the collection indices, not its count. Resuming you should check if your range fits within the collection indices. Check this post. Note that RandomAccessCollection's subscript does NOT return an optional, it would crash if you pass an invalid range.

guard range.clamped(to: anArray.indices) == range else {
return nil
}

let slice = anArray[range]

SwiftUI trouble with index out of range

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:

  • What does “Fatal error: Unexpectedly found nil while unwrapping an Optional value” mean?

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

You should change your numberOfItemsInSection method to:

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.bloodBanksManager.bloodBanksArray.count
}

By returning 3 you are assuming that your array is always three items long, so the exception is thrown every time your bloodBanksArray has more than 3 items.

EDIT - Code optimizations

  1. You could try to optimize your parseJSON function by decoding to an array, avoiding the for loop:
func parseJSON(bloodBankData: Data) {
let decoder = JSONDecoder()

do {
bloodBanksArray = try decoder.decode([BloodBanksData].self, from: bloodBankData)
} catch {
print(error)
}
}

  1. You could change your cellForItemAt method like this, avoiding the multiple access to the same row item.

    Also, use a global variable for the placeholderImage, since it is always the same.

    Finally, try to reduce the value for the maxheight and maxwidth parameters:
let placeholderImage = #imageLiteral(resourceName: "bloodbank4")

...

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.BloodBanks.bloodBankCellIdentifier, for: indexPath) as! BloodBanksCell
let bloodBankItem = self.bloodBanksManager.bloodBanksArray[indexPath.row]
cell.bloodBankName.text = bloodBankItem.name

let maxImageSize = 500
guard let imgUrl = URL(string: "https://maps.googleapis.com/maps/api/place/photo?photoreference=\(bloodBankItem.photo)&sensor=false&maxheight=\(maxImageSize)&maxwidth=\(maxImageSize)&key=XXX") else {
return cell
}
cell.bloodBankImageView.sd_setImage(with: imgUrl, placeholderImage: placeholderImage)

return cell
}

Other reasons for slow loading could be:
3. Slow internet connection
4. The library used for loading the image is not using lazy loading, for more information about lazy loading on UITableView/UICollectionView see this

Why Is My Array Index Out Of Range Using An If Statement?

When you look at the documentation of the insert function, it says the following about the i parameter:

i

The position at which to insert the new element. index must be a valid index of the array or equal to its endIndex property.

You need to insert the element to an existing index or add it to the end of the array. It might help to add a print statement to print index, i and the array you are inserting it in to see what is exactly going on.

Thread 1: Fatal error: Index out of range In SwiftUI

ForEach with an index-based approach is dangerous in SwiftUI. Instead, make your model identifiable.

class User: ObservableObject, Identifiable {
var id = UUID()
//...

Then, change your loop:

ForEach(appUser.friendAnfrage) { item in
SingleFriendView(user: item)
}

Unrelated to this exact issue, but generally SwiftUI does better with using a struct for a model instead of a class. If a User in friends is updated with your current code, because it's a nested ObservableObject, your View will not get automatically updated.



Related Topics



Leave a reply



Submit