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
- 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)
}
}
- 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 themaxheight
andmaxwidth
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 itsendIndex
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
How to Get the Day of the Week With Foundation
How to Programmatically Set Device Orientation in iOS 7
Ios9 Getting Error "An Ssl Error Has Occurred and a Secure Connection to the Server Cannot Be Made"
Uilabel - Auto-Size Label to Fit Text
Uipageviewcontroller and Storyboard
How to Detect Whether a User Has an iPhone 6 Plus in Standard or Zoomed Mode
Do Something Every X Minutes in Swift
Getting iOS System Uptime, That Doesn't Pause When Asleep
Run App For More Than 10 Minutes in Background