iOS app crashing on high numbers of table cells with sliders - ios

Noob here.
I am programmatically creating a number (globalCount) of UISliders in iOS with Swift 3. A grandTotal float value is divided between these sliders' values and when one is changed they all adjust to sum to 100% of the grandTotal. No slider goes below their minimum of 0 or above their maximum of 100.
All is working well for numbers of sliders less than 7. As soon as I add 7 or more. I get a crash when trying to update the cell's slider values with the underlying struct values.
I've moved the cell update functionality to its own function to try isolate the issue. I still get a crash. The crash is
EXC_BAD_INSTRUCTION (code=EXC_1386_INVOP, subcode=0x0)
and appears on the let loopCell line.
Does anyone have any suggestions as to what this is about? Why is it working for low numbers and not higher numbers of cells? Is this a conditionals thing?
This is the cell update function
func updateCells() {
for i in 0...globalCount - 1 {
let loopCell: MyTableCell = ((myTableView.cellForRow(at: IndexPath(row:i, section: 0))) as? MyTableCell)!
loopCell.valueLabel.text = String(structData[i].value)
loopCell.cellSlider.value = structData[i].value
}
}
My UI:

cellForRow(at:) returns nil if a table view cell is not visible (currently offscreen). You are crashing because you are force unwrapping nil. Instead, use optional binding (if let) to safely unwrap the value:
func updateCells() {
for i in 0..<globalCount {
if let loopCell = myTableView.cellForRow(at: IndexPath(row: i, section: 0)) as? MyTableCell {
loopCell.valueLabel.text = String(structData[i].value)
loopCell.cellSlider.value = structData[i].value
}
}
}

It's due to use of force unwrapping it is crashing .
Best handy way is to use if let statement to check data is there or not , since if let does it (unwrapping).

Related

UI Test Case XCUIElementTypeQueryProvider tableRows and tableColumns count is 0

I am trying to test whether the UITableView has at least first cell available or not.
func testFirstCellIsAvailable() throws {
let app = XCUIApplication()
app.launch()
let tablesQuery = app.tables
XCTAssertNotNil(tablesQuery.cells.staticTexts["John"], "First Cell should be John")
}
This is one of the simplest way to find the TableView is loaded or not.
However , I see XCUIElementQuery has tableRows and tableColumns, when I check the value it's always zero. So the question is when do we use tableRows and tableColumns
For the above example value of app.tableRows.count and app.tableColumns.count is 0.
tableColumns and tableRows are usually seen in 2D tables, while "flat" UITableView consist of cells. For example, you can see such elements if you create a table in macOS Notes application and inspect XCUIApplication

Int vs. IndexPath Troubles

I am writing a function to loop through all of the cells in a UITableViewController. Here is what I have so far:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let cell = tableView.cellForRow(at: i-1)
}
Everything loops correctly until I try to get the cell. It expects an input of type IndexPath, however I am passing in an Int. Whenever I force it as an IndexPath like so:
let cell = tableView.cellForRow(at: i-1 as! IndexPath)
I get a warning saying that it will always fail/ always return nil. Is there a better way to do this, or am I just missing a crucial step. Any help is greatly appreciated. Thanks in advance!
EDIT (A LITTLE MORE EXPLANATION): All the cells are custom classed cells, with a specific variable. I want to loop through all of the cells and get that value.
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
print(aCell.question?.text) <------- this is the value I want
}
Most of the information below has been said by others in the other answers and comments, but I wanted to put it all in one place:
We need to step back from the specifics of the question and ask what you are actually trying to do.
The UITableView method cellForRow(at:) will only return cells that are actually on-screen. If there is only room for 5 cells and you have to scroll to expose the rest, that method will return nil for all but the cells that are visible.
As others have suggested, if your goal is to loop through the cells that are on-screen the property tableView.visibleCells would be a better choice.
If your goal is to loop through all cells that exist in your data then you need to explain what you are trying to do.
As for your specific question, the cellForRow(at:) wants a parameter of type IndexPath. You can't simply cast an Int to IndexPath. That will fail. Instead, as #TaylorM said in their answer, you need to create an IndexPath. If your table view only has a single section then you can simply use
let indexPath = IndexPath(row: i, section: 0)
(Assuming you fix your loop code so your indexes start at 0.)
It also does not make sense to use a while loop like that.
Instead of all of this, I suggest using:
let visibleCells = tableView.visibleCells
for aCell in visibleCells {
//Do something with the cell
}
You should create an IndexPath via code, like this:
let ndx = IndexPath(row:i, section: 0)
Or, to modify your code:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
let ndx = IndexPath(row:i-1, section: 0)
let cell = tableView.cellForRow(at:ndx)
}
Based on the later edit where you mention that you want the value of a text string on each row, I would suggest that the above is probably not the best way to approach this :) (I know others have already said this, but I didn't want to make assumptions about what you wanted to do unless you specifically stated what you wanted ...)
You are probably better off taking the same approach you take to populate the data for the table view cells via cellForRowAt: to get the question text than to loop through all the table rows, which would result in some issues for non-visible rows as others have indicated already.
If you have any issues with getting the data provided for cellForRowAt:, do share the code for the cellForRowAt: with us and I'm sure one of us can help you figure things out :)
Try
let indexPath = IndexPath(row: i, section: 0)
You may need to adjust the section to fit your needs.
Check out the documentation for IndexPath. Like most classes and structures, IndexPath has initializers. If you need to create a new instance of any object, you'll most like use an initializer.
*also see the comment from #rmaddy:
"Unrelated to your question but I can almost guarantee that your attempt to loop through all of the cells is the wrong thing to do. What is your actual goal with that loop?"
I believe I have figured it out using all of the help from you wonderful people.
In my case, I have a custom class with a variable that I want access to. I can do what Taylor M, Fahim, and Duncan C said: let indexPath = IndexPath(row: i, section: 0)
Then I can add as! NumberTableViewCell to the end, which allows me access to the values defined there. In the end, it looked like this:
var i = 0
while (i < tableView.numberOfRows(inSection: 0)) {
i += 1
var cell = tableView.cellForRow(at: IndexPath(row: i-1, section: 0)) as! NumberTableViewCell
print(cell.question.text!)
}
Thank you all for your help!

App Crash on Table Row load

I'm pulling data in from CloudKit, and there is only one item in the data.
Once the matchup loads, and before the background color is seen in the UI, the app crashes. I can't figure out why, any ideas?
InterfaceController on Watch:
func loadTable() {
self.rowTable.setNumberOfRows(self.matchupArray.count, withRowType: "rows")
let rowCount = self.rowTable.numberOfRows
for i in 0...rowCount {
let row = self.rowTable.rowController(at: i) as! Rows!
row?.matchup.setText(self.matchupArray[i])
let colorBackground = UIColor.init(hex: self.teamColorArray[i])
row?.groupColor.setBackgroundColor(colorBackground)
}
}
func getData() {
cloud.getCloudKit { (game: [GameWatch]) in
var teamColorArray = [String]()
var matchupArray = [String]()
for item in game {
teamColorArray.append(item.teamColor)
matchupArray.append(item.matchup)
}
self.teamColorArray = teamColorArray
self.matchupArray = matchupArray
self.loadTable()
}
}
UPDATE:
Got a crash, with the error "fatal error: Index out of range".
I'm not sure why this is, because the matchupArray.count is 1, the rowCount is 1. It started iterating through the for-loop with i as 0, and finished the first iteration where it should have stopped since there was only 1 item. But I got the crash because it started to iterate through the loop again, with i as 1, and then obviously found nothing so it crashed.
The crash comes after row?.matchup.setText(self.matchupArray[i]) is run.
The error is in the line:
for i in 0...rowCount
This ... operator creates a range of indexes that includes both values when, because Swift uses 0-based arrays, you need the ..< operator to create a range that excludes the upper value.
This line should therefore be:
for i in 0..<rowCount

App crashes if playlist count = 0 (Do empty playlists have a persistentid?)

My app is crashing if a playlist is empty (no songs). My app works for all non-empty playlists. It seems like there isn't a persistentid for an empty playlist, but I think I am wrong on that.
let qryPlaylists = MPMediaQuery.playlistsQuery()
var selectedPlaylistTitle: String!
var selectedPlaylistPersistentID: UInt64!
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath: NSIndexPath = playlistsTableView.indexPathForSelectedRow!
let rowItem = qryPlaylists.collections![indexPath.row]
let playlistSize = rowItem.count
print("playlistSize = ", playlistSize)
selectedPlaylistTitle = rowItem.valueForProperty(MPMediaPlaylistPropertyName) as! String
print("selectedPlaylistTitle = ", selectedPlaylistTitle)
// Will crash on this next line if the playlistSize = 0
selectedPlaylistPersistentID = rowItem.items[0].persistentID
// If I use following line instead, it will crash on any playlist
// selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
// This line will never be printed
print("selectedPlaylistPersistentID = ", selectedPlaylistPersistentID)
}
Thanks in advance for any help!
If an array such as items is empty, it has no index 0 and will crash if you refer to it, with an out-of-bounds error. So if you don't want to crash, don't do that. You already know that items is empty, because rowItem.count told you so; as you said yourself, playlistSize is 0 in that case.
A simple-minded way to look at it is this: the largest legal index in an array is one less than its count.
Another issue you asked about is that this line always crashes:
selectedPlaylistPersistentID = rowItem.valueForProperty(MPMediaPlaylistPropertyPersistentID) as! UInt64
The problem here is that you are apparently using Swift 2.x. (You should have said that in your question; I deduce it, though, from the fact that valueForProperty has not changed to value(forProperty:), which is what it is called in Swift 3.)
In Swift 2, you cannot cast directly down to UInt64. (I am surprised that the compiler does not draw your attention to this fact with a warning.) Thus, the cast fails and you crash. You need to cast down to NSNumber and then take that NSNumber's unsignedLongLongValue.
And while you are doing this, you really should stop using exclamation marks in your code. When I say "cast down", I mean "cast down safely". My own Swift 2 code for doing this sort of thing looks like this:
if let id = (rowItem.valueForProperty(MPMediaItemPropertyAlbumPersistentID) as? NSNumber)?.unsignedLongLongValue {
// ...
}

NSUserDefault constant not updating

I have NSUserDefaults implemented in my app and the correct values persist between launches, but when referencing a constant I created the values don't update when the default values are changed during a session.
I'm certain my problem stems from using constants to reference NSUserDefaults, but I can see how much code it's saved me and I'd like to stick with this approach, if feasible/good practice. I'm not sure how I'd go about updating the constant for new NSUserDefault values. I'm not getting crashes--just stale values.
My Setup
AppDelegate.swift
I use NSUserDefaults to store some integer values in my app. In AppDelegate.swift I do a check to see if they're set. This works.
PreferencesViewController.swift
I have a PreferencesVC that I've configured that populates with the defaults and enables the user to change the default values. I put some print statements to verify new NSUserDefaults values when they're saved. This works.
MyTableViewController.swift
I declared constants on a so I can trim down verbose code.
// Import Statements
#import UIKit
// Constants I declare here so they're accessible throughout the app.
let kDefaultActiveTime = NSUserDefaults.standardUserDefaults().integerForKey("defaultActiveTime")
let kDefaultBreakTime = NSUserDefaults.standardUserDefaults().integerForKey("defaultBreakTime")
let kDefaultRounds = NSUserDefaults.standardUserDefaults().integerForKey("defaultRounds")
// Class declaration
class MyTableViewController: UITableViewController
Within my cellForRowAtIndexPath I use the default constants to compute a time and output a string for a label on a cell. Here's the line:
// boilerplate cellForRowAtIndexPath omitted
let customCell = tableView.dequeueReusableCellWithIdentifier("customCell", forIndexPath: indexPath) as! RoutineSelectionTableViewCell
let routine = fetchedResultsController.objectAtIndexPath(indexPath) as! SavedRoutines
// ** This doesn't update w/ tableView.reloadData()
customCell.routineInformationLabel.text = String(format: "Time to complete - %#", formatTimeInSeconds(totalRoutineTime(routine.items! as NSOrderedSet)))
Here are the methods used to create the string for routineInformationLabel.text:
// Time computation methods
func totalRoutineTime(routine: NSOrderedSet) -> Int {
var totalRounds = 0
for item in routine {
// get the total of rounds
totalStretchRounds += (((item as! MyNSManagedObject).sideMultiplier?.integerValue)!) * kDefaultRounds
}
let totalActiveTime = totalRounds * kDefaultActiveTime
let totalBreakTime = (totalRounds - 1) * kDefaultBreakTime // subtract 1 rest period for last item
return totalActiveTime + totalBreakTime
}
func formatTimeInSeconds(totalSeconds: Int) -> String {
let seconds = totalSeconds % 60
let minutes = (totalSeconds / 60) % 60
let hours = totalSeconds / 3600
let stringHours = hours > 9 ? String(hours) : "0" + String(hours)
let stringMinutes = minutes > 9 ? String(minutes) : "0" + String(minutes)
let stringSeconds = seconds > 9 ? String(seconds) : "0" + String(seconds)
if hours > 0 {
return "\(stringHours):\(stringMinutes):\(stringSeconds)"
}
else {
return "\(stringMinutes):\(stringSeconds)"
}
}
I tried creating an observer to reload the tableView when NSUserDefaults are saved, but that doesn't seem to be doing the trick. I've also tried throwing in tableView.reloadData() in viewDidAppear, viewWillAppear, etc. w/o much luck.
Thank you for reading. I welcome your suggestions.
It appears that all the constants are really giving you is a little cleaner, more readable code. I would recommend writing private helper functions in the class to do the same thing. Reading in and out of the NSUserDefaults is pretty quick and shouldn't present a problem. I also agree with the comments on the other answer, there is no need to synchronize. It's usually recommended against. Let the iOS system handle this for you. It's plenty capable of doing so. Syncronizing the defaults writes them to disk, it will do this when it is ready. Otherwise, it will keep and read the data from memory.
I don't see any glaring errors in your code shown, but my best guess is that at some point the defaults are getting updated and the constants have already been set. So basically, they'll never get set again once the class has been loaded.
To get the values in more concise, easily typed manner, consider a private function like so:
private func getDefaultRounds() -> Int {
return NSUserDefaults.standardUserDefaults().integerForKey("defaultRounds")
}
Which could be accessed any where in the class with just getDefaultRounds()
I have accepted Kyle's solution, but I experimented a little as a learning exercise and I'm throwing this up to see what folks think:
Rather than type the code repeatedly, remember what my key is, whether it's an Int, a Float, or a Bool, etc., I created a Struct to access the values:
struct Defaults {
let activeTime = NSUserDefaults.standardUserDefaults().integerForKey("defaultActiveTime")
let breakTime = NSUserDefaults.standardUserDefaults().integerForKey("defaultBreakTime")
let rounds = NSUserDefaults.standardUserDefaults().integerForKey("defaultRounds")
}
When I need to access the NSUserDefaults values, they're updated dynamically. It seems declaring them as a constant "snapshots" them in time, but they don't update.
When I need a default and I don't want to type them, I type:
Defaults().rounds
Defaults().activeTime
Defaults().breakTime
I haven't stumbled into any "gotchas" with this approach...yet. If anyone has suggestions on how to improve on this, I'm all ears.

Resources