For some reason HTML is always dirty, it’s usually full of Analytics tags, JavaScript or contains nested HTML tags. This is usually fine for displaying in browsers but at some point an iOS app will need to display HTML content and usually when it does, you need it to display clean HTML or only a small subset of HTML tags… all it takes is an unexpected tag and the whole document layout could be ruined. So here is a way of quickly and easily stripping HTML content down.
Continue reading
Cocoa Touch
There are 5 posts tagged Cocoa Touch (this is page 1 of 1).
Core Animation stops animation on app relaunch
On one of my projects I discovered a bug in a never-ending animation I had set up. Whenever the app was suspended (such as when you multitask and open another app), on relaunching the app the animation was frozen. After some investigating, I discovered that with Core Animation you need to set a flag on the CABasicAnimation class called removedOnCompletion to NO otherwise the animation will get cleaned up when it gets suspended.
Objective-C:
CABasicAnimation *dartWiggleAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
dartWiggleAnimation.fromValue =
[NSValue valueWithCGPoint:CGPointMake(self.center.x-DART_WIGGLE_HALF, self.center.y+DART_CENTER_H_OFFSET)];
dartWiggleAnimation.toValue =
[NSValue valueWithCGPoint:CGPointMake(self.center.x+DART_WIGGLE_HALF, self.center.y+DART_CENTER_H_OFFSET)];
dartWiggleAnimation.timingFunction =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
dartWiggleAnimation.duration = 0.4;
dartWiggleAnimation.repeatCount = HUGE_VALF;
dartWiggleAnimation.autoreverses = YES;
dartWiggleAnimation.removedOnCompletion = NO;
[[nextDart layer] addAnimation:dartWiggleAnimation forKey:@"dartWiggle"];
Swift:
let dartWiggleAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.position))
dartWiggleAnimation.fromValue = CGPoint(x: self.center.x - DART_WIGGLE_HALF, y: self.center.y + DART_CENTER_H_OFFSET)
dartWiggleAnimation.toValue = CGPoint(x: self.center.x + DART_WIGGLE_HALF, y: self.center.y + DART_CENTER_H_OFFSET)
dartWiggleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
dartWiggleAnimation.duration = 0.4
dartWiggleAnimation.repeatCount = Float.greatestFiniteMagnitude
dartWiggleAnimation.autoreverses = true
dartWiggleAnimation.isRemovedOnCompletion = false
nextDart.layer.add(dartWiggleAnimation, forKey: "dartWiggle")
Does this seem like a bug or a feature?
WWDC Update: 5 key points for iPhone apps
It’s now been just over 3 weeks since the torrent of information unleashed at Apple’s World Wide Developer Conference refreshingly drenched the brains of designers, developers and engineers. I’ve resisted blogging about the public announcements to fully let the impact soak in and gage everyone’s reactions but now feels like a good time to talk about where the future of computing is heading.
Continue reading
Core Data Objects in Wrong Sections
NSFetchedResultsController is a really handy class. Use one of the default Core Data templates in Xcode and you’ll very quickly have a nice list of managed objects in a table view. With a few more lines of code you can get the NSFetchedResultsController to group your objects by sections. You do this by specifying a key-path in the class’s constructor method but there is another step that if overlooked will cause some confusion.
In a sample app I’ve created a food table that lists food in categories.
FetchedResultsController method grouping sections using a key-path:
Objective-C:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create and configure a fetch request with the food entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"RWFood" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"category" cacheName:@"Food"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
[aFetchedResultsController release];
[fetchRequest release];
[nameDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Swift:
lazy var fetchedResultsController: NSFetchedResultsController<rwfood> = {
let fetchRequest: NSFetchRequest<rwfood> = RWFood.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "category", cacheName: "Food")
aFetchedResultsController.delegate = self
do {
try aFetchedResultsController.performFetch()
} catch let error {
print("Unable to perform fetch: \(error)")
}
return aFetchedResultsController
}()
Specify a key-path
Save and quit the app a few times and you’ll see the objects seem to be in the wrong sections. If you look closer you’ll see that the objects are actually sorted in ascending name order. On looking at the code, it seems this is exactly what we asked the program to do! After some testing it also seems to show up more often if the table is a grouped one.
As per the docs, after you specify a key-path to group each section with you also need to make sure the first sort descriptor is sorting this key-path. Add a sort descriptor and everything will work as expected.
Revised fetchedResultsController method with missing sort descriptor:
Objective-C:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create and configure a fetch request with the plant entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"RWPlant" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *typeDescriptor = [[NSSortDescriptor alloc] initWithKey:@"type" ascending:YES];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:typeDescriptor, nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:@"type" cacheName:@"Plants"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
[aFetchedResultsController release];
[fetchRequest release];
[categoryDescriptor release];
[nameDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
Swift:
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest: NSFetchRequest<rwplant> = RWPlant.fetchRequest()
let typeDescriptor = NSSortDescriptor(key: "type", ascending: true)
let nameDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [typeDescriptor, nameDescriptor]
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "type", cacheName: "Plants")
aFetchedResultsController.delegate = self
do {
try aFetchedResultsController.performFetch()
} catch let error {
print("Unable to perform fetch: \(error)")
}
return aFetchedResultsController
}()
Re-ordering Core Data Objects on iOS4
The Core Data framework on the iPhone is incredibly powerful. It’s also incredibly efficient and part of that is because a collection of objects only have the order that you implicitly give them. For example you typically might give an Event object a timestamp and when you pull out all the events you might sort on that timestamp.
The NSFetchedResultsController
is the main power house when dealing with such a scenario and is great for the master part of a master/detail data relationship. Its main purpose is to manage the results returned from a fetch request similar to the above and provide data for a UITableView
via delegate methods. It reacts on the model level so if you delete an Event object, the NSFetchedResultsController
informs its delegate and so updates the UITableView
automatically. It’s very clever indeed!
As I found out earlier yesterday, the problem comes when you want to re-order the objects in a user-defined way. Instead of sorting on a timestamp, I wanted the user to be able to specify that one object should occur before the other… I’m working on an app that lets you place waypoints down on a map. Timestamps in waypoints aren’t much use. It’s much more critical that they have a specific order.
After some Googling I came across a useful article on CocoaIsMyGirlfriend. This helped me 90% of the way but I had problems when re-ordering. When you re-order the objects, the UITableView would move cells about seemingly at random. This is because NSFetchedResultsController
is model-driven. When you re-order something using the tableview methods the view is already correct (because you’ve dragged and dropped the cell there – it’s a user-driven change) and so when the delegate detects your index changes, it walks all over your view believing the cells to be in their original position. The trick is to ‘disable’ the UI updates with a boolean in your delegate methods. Set the boolean just before your re-indexing and unset it afterwards.
For an example, see this stack overflow link on re-ordering.
Also have a look at what the Apple docs say on user-driven updates on the NSFetchedResultsController
.