I want to randomly display some images stored as attributes on an entity "Disc" in Core Data. I've been using the Swift Array.shuffled() function performed on the fetchedObjects of my NSFetchedResultsController "thisFRC."
First problem was that the desired images often did not appear when they should. Here's code that produced this problem. It's part of a function loadImages, which is called from viewWillAppear:
try thisFRC.performFetch()
fetchedData = thisFRC.fetchedObjects as! [Disc]
shuffledDiscs = fetchedData.shuffled()
thisDisc = shuffledDiscs [0]
Second problem was that when they did appear, I would very often see the same image repeated several (or many) times. I thought maybe the images were persisting for some unknown reason, so I did:
frontImageView.image = nil
rearImageView.image = nil
in prepareForSegue. Same problem upon returning to the original View Controller.
Third problem arose when I tried to fix the second problem by further randomizing the order of the images with the code below. It crashes at the commented line with this error: “Index out of range”.
try thisFRC.performFetch()
fetchedData = thisFRC.fetchedObjects as! [Disc]
shuffledIndices = fetchedData.indices.shuffled()
index = shuffledIndices [0] // Crashes here with “Index out of range”
shuffledDiscs = fetchedData.shuffled()
thisDisc = shuffledDiscs [index]
My questions:
1) Why doesn't the shuffled() function do a better job of randomizing? To be fair, I tried this code in a separate test app, and it seemed to work fine. If I can clear this up, I can dispense with my workaround.
2) I don't understand how the index could be out of range in index = shuffledIndices [0]
Note:
The images I'm using are quite large -- on the order of 2400 x 2400 -- being squeezed into a 160 x 160 image view, so the early anomalies could possibly be caused by scaling.
Any help would be greatly appreciated!
TIA
Resolved!
With insight from #Adis, and a thousand print() lines, I discovered the problem:
There was a function extraneously inserting a new Disc entity into my context every time I segued to one of the two connected View Controllers. This accounted for the "ghost" items that were showing up with no UIImage attributes, creating blank ImageViews. Once I deleted these anomalies, and killed the function that created them, everything works fine. I was also able to just use the shuffled() function without the extra flourish I had put on it.
All is well, and thanks to all who took the time to look!
Related
I am experimenting with the (Xcode 7) UI XCTestCase test cases and I just stumbled onto an issue with one UIView, in which I have a UITableView with many cells(4000+).
When the app is running normally, only the visible cells are rendered and there is no performance issue at all.
However, if I run the app within the context of recording a XCTestCase and I navigate to this screen, the simulator freezes, apparently because each single cell is rendered as if it were visible.
If I try to script the navigation manually and I run the XCTestCase, the test case fails right after navigating to this screen, exiting with a "UI Testing Failure - Failed to get refreshed snapshot", apparently again because all cells are being rendered and this does not finish in time.
I think this has to do with the fact that the testing framework builds an entire metamodel of the screen under display, adding each of the 4000+ cells into the view tree hierarchy.
I tried adding an expectation, hoping this would give the testing container enough time to finish rendering all cells, but this does not work.
Is there a workaround for this? Is it somehow possible to skip building part of the UI tree hierarchy or something?
My goal is being able to write UI tests for this screen.
You might be able to avoid having the entire table render, if you can use firstMatch instead of element, and also avoid count.
I had a test that checks for expected labels in the first two cells of a table. At first, I was using app.table.cells.element(boundBy: 0) and app.table.cells.element(boundBy: 1) to find the first and second cells. This was resulting in the whole table being rendered before I could access the cells.
I adapted my test to be slightly less precise, but still good enough for me (given the huge amount of time it would take otherwise). Instead, I use matching with predicates on the expected label values, with firstMatch, to find the first cells matching the criteria I want. This way the traversal stops as soon as it finds them (and since they're at the top of the table, it's quick).
Here's the code before and after.
Before (slow, yet more precise):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.element
let cell0 = table.cells.element(boundBy: 0)
let cell1 = table.cells.element(boundBy: 1)
let actualRhyme0 = cell0.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
let actualRhyme1 = cell1.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
XCTAssertEqual(expectedFirstRhyme, actualRhyme0, "Expected first rhyme for \(query) to be \(expectedFirstRhyme) but found \(actualRhyme0)")
XCTAssertEqual(expectedSecondRhyme, actualRhyme1, "Expected first rhyme for \(query) to be \(expectedSecondRhyme) but found \(actualRhyme1)")
}
Faster, but less precise (but good enough):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.firstMatch
let label0 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedFirstRhyme)).firstMatch
let label1 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedSecondRhyme)).firstMatch
// We query for the first cells that we find with the expected rhymes,
// instead of directly accessing the 1st and 2nd cells in the table,
// for performance issues.
// So we can't add assertions for the "first" and "second" rhymes.
// But we can at least add assertions that both rhymes are visible,
// and the first one is above the second one.
XCTAssertTrue(label0.frame.minY < label1.frame.minY)
XCTAssertTrue(label0.isHittable)
XCTAssertTrue(label1.isHittable)
}
Reference:
https://developer.apple.com/documentation/xctest/xcuielementquery/1500515-element
Use the element property to access a query’s result when you expect a
single matching element for the query, but want to check for multiple
ambiguous matches before accessing the result. The element property
traverses your app’s accessibility tree to check for multiple matching
elements before returning, and fails the current test if there is not
a single matching element.
In cases where you know categorically that there will be a single
matching element, use the XCUIElementTypeQueryProvider firstMatch
property instead. firstMatch stops traversing your app’s accessibility
hierarchy as soon as it finds a matching element, speeding up element
query resolution.
I had the same issue, and I agree it is frustrating having to wait for the entire table to load, but that is what I had to do using the following workaround.
This may not be what you are looking for but it may help others:
Basically I am counting the cells in the table 2 times consecutively if they are not equal that means the table is still loading. Put it in a loop it and do that until both counts return the same number which would mean the table is finished loading. I then put in a stop of 30 seconds so that if this takes longer than 30 seconds, the test will fail (this was enough time in my case). If your table will take longer than that you could increase the number to 180 for 3 mins etc...
let startTime = NSDate()
var duration : TimeInterval
var cellCount1 : UInt = app.tables.cells.count
var cellCount2 : UInt = app.tables.cells.count
while (cellCount1 != cellCount2) {
cellCount1 = app.tables.cells.count
cellCount2 = app.tables.cells.count
duration = NSDate().timeIntervalSince(startTime as Date)
if (duration > 30) {
XCTFail("Took too long waiting for cells to load")
}
}
//Now I know the table is finished loading and I can tap on a cell
app.tables.cells.element(boundBy: 1).tap()
I see questions regarding long delays in displaying UIImageViews after downloading, but my question involves long delays when
reading from local storage.
After archiving my hierarchy of UIImageViews to a local file (as per narohi's answer in
How to output a view hierarchy & contents to file? ),
I find that if I want to reload them, it takes 5 to 20 seconds for the views to actually appear on screen,
despite my setting setNeedsDiplay() on the main view and all the subviews.
I can immediately query the data contained in the
custom subclasses of UIView that get loaded -- showing that NSKeyedUnarchiver and all the NS-decoding and all the init()'s have completed -- however
the images just don't appear on the screen for a long time. Surely the next redraw cycle is shorter than 5-20 seconds...?
It seems odd that images from PhotoLibrary appear instantly, but anything loaded from local file storage using NSKeyedUnarchiver takes "forever."
What's going on here, and how can I speed this up?
.
.
To be explicit, the relevant part of my Swift code looks like this:
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
myMainView.addSubview(view)
view.setNeedsDisplay()
// now do things with the data in view ...which all works fine
I find that, even if I add something like...
for subview in view.subviews {
subview.setNeedsDisplay()
}
...it doesn't speed up the operations.
We are not talking huge datasets either, it could be just a single imageview that's being reloaded.
Now, I do also notice these delays occurring when downloading from the internet using a downloader like the one shown in
https://stackoverflow.com/a/28221670/4259243
...but I have the downloader print a completion message after not only the download but when the (synchronous operation)
data.writeToFile() is complete (and before I try to load it using NSKeyedUnarchiver), so this indicates that the delay
in UIImageView redraws is NOT because the download is still commencing....and like I say, you can query the properties of the data and it's all in memory, just not displaying on the screen.
UPDATE: As per comments, I have enclosed the needsDisplay code in dispatch_async as per Leo Dabus's advice, and done some Time Profiling as per Paulw11's. Link to Time Profiling results is here: https://i.imgur.com/sa5qfRM.png I stopped the profiling immediately after the image appeared on the screen at around 1:00, but it was actually 'loaded' during the bump around 20s. During that period it seems like nothing's happening...? The code is literally just waiting around for a while?
Just to be clear how I'm implementing the dispatch_async, see here:
func addViewToMainView(path: String) {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
...Since posting this I've found a few posts where people are complaining about how slow NSKeyedUnarchiver is. Could it just be that? If so, :-(.
SECOND UPDATE: Ahh, the "let view = " needs to be in the dispatch_async. In fact, if you just put the whole thing in the dispatch_async, it works beautifully! so...
func addViewToMainView(path: String) {
dispatch_async(dispatch_get_main_queue(), {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
This works instantly. Wow.. Credit to Leo Dabus. Leaving this here for others...
In my game, I have a function that builds the world map and configures associated data structures. I've been calling it from the the associated View Controllers' viewWillAppear function for months, and everything works as intended.
Today, I got the bright idea to move the world setup call to viewDidAppear and then display progress indicators as it does its thing. At that point, the setup function began throwing exceptions about 98.5% of the time. The few times it actually worked, I received memory warnings. Moving the call back to viewWillAppear solved the problem (and warnings) completely. But I couldn't let it go that easily and continued to debug.
I have this segment that decides where to build rivers. In that segment, there's a check for the altitude of neighboring cells to ensure rivers always flow downhill. In that segment I simply used bubble sort of the four (non-corner) neighbors, which is called while evaluating possible river locations:
for (int i=0; i<4; i++)
{
for (int j=0; j<4-i; j++)
{
if (candidates[j].tileType > candidates[j+1].tileType)
{
struct riverCandidate temp = candidates[j+1];
candidates[j+1] = candidates[j];
candidates[j] = temp;
}
}
}
Eventually, I got far enough to NSLog the values of the candidate[ ]'s before and after the sort. What I found is that when this whole thing is called from viewWillAppear, it works flawlessly 100% of the time. However, when called from viewDidAppear, occasionally one of the candidate[ ] elements is randomly corrupted (resulting in the aforementioned exception when I subsequently try to access it). I can't say I detected any clear patterns of corruption except that a) only one element of the 4 were ever corrupted at a time and b) I only saw the first [0] and last [3] elements corrupted. This chunk of code may be called a few dozen times in the course of world setup, and most passes are successful... but it only takes one of those passes to fail to screw up the works downstream.
To fix it, all I did was move the definition of temp outside the loop, like so:
struct riverCandidate temp;
for (int i=0; i<4; i++)
{
for (int j=0; j<4-i; j++)
{
if (candidates[j].tileType > candidates[j+1].tileType)
{
temp = candidates[j+1];
candidates[j+1] = candidates[j];
candidates[j] = temp;
}
}
}
Now it's working 100% of the time when called from viewDidAppear, and no men warnings.
So here are my questions: why would behavior change between viewWillAppear and viewDidAppear? If there's a potential race condition with malloc'ing the temp structure, why would it never happen from viewWillAppear and virtually always happen in viewDidAppear?
I'd like to understand this behavior better so that I don't stumble into it again.
UPDATE: So moving the definition of temp didn't actually help. Guess I just got lucky with a series of successful runs that made it appear things were working. After more debugging, I finally noticed the j loop above has a typo that lets it write beyond the end of the array, and corrected that.
But that still leaves me confused: how did I never hit that when called from viewWillAppear, but always hit it when I moved the function call to viewDidAppear? I must be missing something obvious.
Thanks
I've been trying to figure out UIImage for some time now. I've been trying to figure out an approach to having one view the 'Main Game View' and showing either 2/3/4 different images depending on the 'level' variable. I'm just trying to be sure of logic. So for example level 1 would display 4 pictures and level 2 might display 3 different pictures. I don't want to hinder performance of the app but because the game is to be played offline and archiving won't make much of a difference all the images (several hundred optimised images) are being stored locally in the main app bundle.
I'm just wondering if my logic for trying to implement this so far is sound or not. For level 1 I would implement the 4 UIImageViews needed and initialise them with images, then display them on screen at set positions. I would then preload the next levels images using GCD. When a continue button is pressed I will set the UIImages and the UIImageViews to nil and display level 2's (or the next level) on screen.
I'm not confident in my approach and was wondering if there was something that would make it simpler or something I've missed or even if in practice it will work accordingly to the theory.
Thank you in advance for you time and any help.
Sorry if this is unclear.
I'm assuming you use a single view controller class for all your levels and load the levels from some .plist file or other format.
Don't bother using GCD. Loading 4 images once per level during loading costs practically nothing.
If all you need to do is have 2-4 images on the screen for each level (in addition to any other level elements), simply create 4 UIImageView instances and add them to your view. Then when you load a level, create new UIImage objects from the required image files and set your UIImageViews' image property to them. The UIImage objects of the old level will be released and deallocated at that moment, since you (and the UIImageViews) don't have a strong reference to them anymore.
Pseudo implementation:
- (void)loadLevel:(int)levelNumber {
// Assuming you have your image views in an array imageViews.
// Determine the image files required for this level.
// Put them into an array imageNames.
for (int i = 0; i < 4; i++) {
if (imageNames.count < i) {
UIImage *image = [UIImage imageNamed:imageNames[i]];
[self.imageViews[i] setImage:image];
} else {
[self.imageViews[i] setImage:nil];
}
}
Edit: While my comments have an iOS 5 working example, I am still getting this for other versions. I've now implememted a test to only register and dequeue cells if iOS 5, but it's really puzzling!
still receiving _accessibilityUpdateRemoveControl exceptions, strange nuisance, appears to be something with the edit controls, nothing is retained so nothing needs deallocing, but will try, and post the answer if I find it!
This was working yesterday, and now it's not... I changed nothing!
Edit: Turns out, while reloadData causes the crash, the crash does not occur without my custom tableViewCell... hmmm, something about removing the + sign, but it doesn't happen with deletion!
Actual error is this:
[CustomTableViewCell _accessibilityUpdateRemoveControl]: message sent to deallocated instance.
What's funny is, the remove button works. Essentially it removes the item from an array, adds it to another, basically putting it "to another table". No crashing, works fine.
If I remove the line that reloads the data in the table, after the insert button adds it, it also works. Eg: Don't immediately reload the data, close window, come back, everything displays fine. The exact line, so far, that crashes it is in
[theTable reloadData], but that line, for the other table (as I update both) doesn't crash at all. Actually, thanks to that, I'm gonna view the headers for UITableView's functions, and view other answers with that specific line. I just didn't see this, anywhere, after searching for that weird function call.
I'm ensuring my cell is within memory, and even quit dequeuing just to ensure it's working. I'm stumped with this, hopefully will have solution in an hr or less.
Thanks
I stepped through Apple's code, line by line, read the name of every function and noticed this:
editControlWasClicked: (id) clicked
is called just before crashing. I combined that with the error message, and the fact I call [table2 reloadData] before this is called, and pieced those pieces together.
The cell is erased (so it moves to the other table), but somehow calls its system callBack "editControlWasClicked" after the table reloads... since it's on the main thread, I'm guessing the table stuff is multi-threaded... how else would it call these in order but do that After the reload??
So to test this, I used the "afterDelay" function, and low and behold, it worked.
You may be asking why I'm using an add edit control in one and subtract in the other... there is a purpose to that.
So, possible solutions: 1) use the afterDelay method of selectors.
2) Write a custom IBAction ('cause it's a xib) or otherwise use custom images and functions to ensure that doesn't get called back.
Note, 2 involves writing an extra delegate so that messages from the cell can reach the view controller.
Basic solution: use iOS 5, use the queuing, otherwise do one of the above solutions or figure out the threading/hooks and find a way to do this without delaying. (I would prefer such if I can find it)