SEAS released updated version of media player—DozeGuard, for iPhone and iPod touch. The app allows users listen to their audio files while at the same time monitors whether they are still awake. It is designed to turn off when the user begins falling asleep. If the device is not moved by the user after a certain time, the app stops playing the track but remembers the position, and if the same track is selected next time it continues at the same point. Users can also design a list of titles to be played and have the ability to store the list for use at a later date.
Features:
* Allows to select a list of podcast, music and audio book titles
* Stores the list of selected titles across application starts
* Stores the position of the last 10 titles which were stopped automatically
* Displays title artwork
* Darkens the screen during the play
* Gently moving the device is sufficient to prove alertness
* Jump to position of last move with a button click
* Jump to position of auto stop with a button click
* Use position slider to find exact position
* Use specific loudness level per audio book title (10 titles)
Device Requirements:
* iPhone and iPod touch
* Requires iPhone OS 3.1.3 or later
* 1.7 MB
Pricing and Availability:
DozeGuard 1.1 is available for 0.99 (USD) worldwide exclusively through the App Store.
Wednesday, March 31, 2010
Tuesday, March 30, 2010
Another SuperDB Validation Tweak
In ManagedObjectAttributeEditor.m, there is this method:
Now, if you say you're going to fix, and then don't, the context never gets rolled back. It should really be:
Since we don't have bindings, the incorrect value shown in the GUI will stay there, we're just removing it from the context. When they hit Save again, we'll copy the value back into the context, but if they hit cancel, we won't leave an invalid value sitting in the unsaved context.
...
#pragma mark -
#pragma mark Alert View Delegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == [alertView cancelButtonIndex]) {
[self.managedObject.managedObjectContext rollback];
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
Now, if you say you're going to fix, and then don't, the context never gets rolled back. It should really be:
...
#pragma mark -
#pragma mark Alert View Delegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
[self.managedObject.managedObjectContext rollback];
if (buttonIndex == [alertView cancelButtonIndex]) {
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
Since we don't have bindings, the incorrect value shown in the GUI will stay there, we're just removing it from the context. When they hit Save again, we'll copy the value back into the context, but if they hit cancel, we won't leave an invalid value sitting in the unsaved context.
Monday, March 29, 2010
Well, That's Embarrassing…
In the SuperDB application in More iPhone 3 Development, when we added multi-attribute validation, we inadvertently stopped the single-field validation on birthdate to stop working. In Hero.m, we have this at the end:
The method validateNameOrSecretIdentity: does our cross-field validation by looking at the two fields, but it doesn't do the rest of the validations, such as those from the data model, or those from custom validation methods. To handle those, we need a call to super.
The easiest way to handle this is to simply call super if validateNameOrSecretIdentity: returns NO. Typically, once we hit an error, we don't keep going. We could implement a more complex version that kept a running track of all errors and returned them, but I'm going to keep things simple here. Replace the code above with the following to get the rest of the validations working again:
I apologize for that!
...
- (BOOL)validateForInsert:(NSError **)outError {
return [self validateNameOrSecretIdentity:outError];
}
- (BOOL) validateForUpdate:(NSError **)outError {
return [self validateNameOrSecretIdentity:outError];
}
@end
The method validateNameOrSecretIdentity: does our cross-field validation by looking at the two fields, but it doesn't do the rest of the validations, such as those from the data model, or those from custom validation methods. To handle those, we need a call to super.
The easiest way to handle this is to simply call super if validateNameOrSecretIdentity: returns NO. Typically, once we hit an error, we don't keep going. We could implement a more complex version that kept a running track of all errors and returned them, but I'm going to keep things simple here. Replace the code above with the following to get the rest of the validations working again:
...
- (BOOL)validateForInsert:(NSError **)outError {
BOOL validated = [self validateNameOrSecretIdentity:outError];
if (!validated)
return validated;
return [super validateForInsert:outError];
}
- (BOOL)validateForUpdate:(NSError **)outError {
BOOL validated = [self validateNameOrSecretIdentity:outError];
if (!validated)
return validated;
return [super validateForUpdate:outError];
}
@end
I apologize for that!
iPhone SDK 3.2 GM Seed
The GM release of the iPhone SDK 3.2 is available in iPhone Dev Center (login required). Even though this build is labeled as "GM" and this will probably be the final build before the iPad gets into the hands of consumers, and even though the official policy is that development material is covered by NDA until it goes GM, this build appears to still be under NDA, which means no iPad technical posts yet. Sorry.
The release notes and license agreement are pretty clear about the fact that this is still considered pre-release software, so the NDA is still intact and we have to wait a few more days before we can start discussing the nuances of iPad development. I'm assuming that come April 3rd, it will be considered "released". At least, I hope we don't have a repeat of the original iPhone NDA situation.
The release notes and license agreement are pretty clear about the fact that this is still considered pre-release software, so the NDA is still intact and we have to wait a few more days before we can start discussing the nuances of iPad development. I'm assuming that come April 3rd, it will be considered "released". At least, I hope we don't have a repeat of the original iPhone NDA situation.
You can explore thousands of DJ mixes on your iPhone! Don't miss this opportunity, download Mix.DJ Pro 2 NOW!
DigitalDeejay announced the immediate availability of new version of Mix.DJ Pro. Mix.DJ Pro 2.0 is an iPhone and iPod touch application that streams over 16,000 pro and amateur DJ mixes from around the world in one click.
The latest version offers improved stability and faster performance, and you can listen over 16,000 DJ mixes anytime and anywhere via WIFI or 3G. In addition, if you find songs inside a mix that you like, just click on the song and it will be sent to iTunes where you can purchase the download of that song. Mix.DJ allows you to select your favorite mixes and save them to "My Favorites".
Users can browse mixes based on style, genre, artist, mood, and more.
Song mixes are available from amateur and professional DJs such as David Vendetta, Dim Chris, Mark Farina, Superfunk, Benjamin Braxton, Jay-J, Kevin Yost, Alan master T, and Michael Kaiser and others.
With this App in your iPhone/iPod you are able to:
* Listen to over 16,000 DJ mixes instantly at your fingertips
* Select from 23 moods and genres pre-programmed to start your listening with one click
* Navigate through over 70 sub-genres of music with ease
* Find mixes, artists or particular songs via the search function
* Create your playlist as many times as you want with the "Favorites" feature
* Browse any playlist mix "Track by Track" with ease
* Buy your favorite tunes directly through the iTunes Store
Requirements:
* iPhone and iPod touch
* Requires iPhone OS 3.1 or later
* 0.3 MB
Pricing and Availability:
Pro 2.0 is priced at US$0.99 and is available at Apple's iTunes based App Store.
The latest version offers improved stability and faster performance, and you can listen over 16,000 DJ mixes anytime and anywhere via WIFI or 3G. In addition, if you find songs inside a mix that you like, just click on the song and it will be sent to iTunes where you can purchase the download of that song. Mix.DJ allows you to select your favorite mixes and save them to "My Favorites".
Users can browse mixes based on style, genre, artist, mood, and more.
Song mixes are available from amateur and professional DJs such as David Vendetta, Dim Chris, Mark Farina, Superfunk, Benjamin Braxton, Jay-J, Kevin Yost, Alan master T, and Michael Kaiser and others.
With this App in your iPhone/iPod you are able to:
* Listen to over 16,000 DJ mixes instantly at your fingertips
* Select from 23 moods and genres pre-programmed to start your listening with one click
* Navigate through over 70 sub-genres of music with ease
* Find mixes, artists or particular songs via the search function
* Create your playlist as many times as you want with the "Favorites" feature
* Browse any playlist mix "Track by Track" with ease
* Buy your favorite tunes directly through the iTunes Store
Requirements:
* iPhone and iPod touch
* Requires iPhone OS 3.1 or later
* 0.3 MB
Pricing and Availability:
Pro 2.0 is priced at US$0.99 and is available at Apple's iTunes based App Store.
My Last Word on NSFetchedResultsController
Somehow, I never posted my final fix for the various NSFetchedResultsController problems. I submitted a bug report to Apple to get the actual row counts out of NSFetchedResultsController including any deferred inserts and deletes, but went ahead with developing a workaround that keeps track of the inserts and deletes locally. I thought I had posted this information, but I guess I never did (Sorry!). I am optimistic that these issues will be fixed in 3.2, but haven't had time to really run the new version of NSFetchedResultsController through its paces, and even if I had, the NDA would keep me from being able to tell you what I had found. Nevertheless, the fact that it has been updated makes me optimistic that this workaround is temporary and that Apple has finally shipped a production-ready version of NSFetchedResultsController. In the meantime…
I'm going to give the changes in context of the Chapter 7 application from More iPhone 3 Development, but these changes are generic and could just be copied to any project using an NSFetchedResultsController and the delegate methods from Chapter 2. I don't usually encourage copy-and-paste coding, but for a temporary workaround it makes sense.
In HeroListViewController.h, we have to add an instance variable to keep track of the deferred inserts and deletes:
Then, we need to make some changes to two of the NSFetchedResultsController delegate methods. Easiest thing is to probably just replace the existing version with these new versions. The only difference from the previous version is that we keep track of insertions and deletions for each transaction and then use the row count for sections is determined by adding the deferred insertion / deletion count to the number reported by the NSFetchedResultsController:
You can find the fixed version of the Chapter 7 application right here.
I'm going to give the changes in context of the Chapter 7 application from More iPhone 3 Development, but these changes are generic and could just be copied to any project using an NSFetchedResultsController and the delegate methods from Chapter 2. I don't usually encourage copy-and-paste coding, but for a temporary workaround it makes sense.
In HeroListViewController.h, we have to add an instance variable to keep track of the deferred inserts and deletes:
#import <UIKit/UIKit.h>
#define kSelectedTabDefaultsKey @"Selected Tab"
enum {
kByName = 0,
kBySecretIdentity,
};
@class ManagedObjectEditor;
@interface HeroListViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UITabBarDelegate, UIAlertViewDelegate, NSFetchedResultsControllerDelegate>{
UITableView *tableView;
UITabBar *tabBar;
ManagedObjectEditor *detailController;
@private
NSFetchedResultsController *_fetchedResultsController;
NSUInteger sectionInsertCount;
}
@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) IBOutlet UITabBar *tabBar;
@property (nonatomic, retain) IBOutlet ManagedObjectEditor *detailController;
@property (nonatomic, readonly) NSFetchedResultsController *fetchedResultsController;
- (void)addHero;
- (IBAction)toggleEdit;
@end
Then, we need to make some changes to two of the NSFetchedResultsController delegate methods. Easiest thing is to probably just replace the existing version with these new versions. The only difference from the previous version is that we keep track of insertions and deletions for each transaction and then use the row count for sections is determined by adding the deferred insertion / deletion count to the number reported by the NSFetchedResultsController:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
sectionInsertCount = 0;
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate: {
NSString *sectionKeyPath = [controller sectionNameKeyPath];
if (sectionKeyPath == nil)
break;
NSManagedObject *changedObject = [controller objectAtIndexPath:indexPath];
NSArray *keyParts = [sectionKeyPath componentsSeparatedByString:@"."];
id currentKeyValue = [changedObject valueForKeyPath:sectionKeyPath];
for (int i = 0; i < [keyParts count] - 1; i++) {
NSString *onePart = [keyParts objectAtIndex:i];
changedObject = [changedObject valueForKey:onePart];
}
sectionKeyPath = [keyParts lastObject];
NSDictionary *committedValues = [changedObject committedValuesForKeys:nil];
if ([[committedValues valueForKeyPath:sectionKeyPath] isEqual:currentKeyValue])
break;
NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (tableSectionCount + sectionInsertCount != frcSectionCount) {
// Need to insert a section
NSArray *sections = controller.sections;
NSInteger newSectionLocation = -1;
for (id oneSection in sections) {
NSString *sectionName = [oneSection name];
if ([currentKeyValue isEqual:sectionName]) {
newSectionLocation = [sections indexOfObject:oneSection];
break;
}
}
if (newSectionLocation == -1)
return; // uh oh
if (!((newSectionLocation == 0) && (tableSectionCount == 1) && ([self.tableView numberOfRowsInSection:0] == 0))) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newSectionLocation] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount++;
}
NSUInteger indices[2] = {newSectionLocation, 0};
newIndexPath = [[[NSIndexPath alloc] initWithIndexes:indices length:2] autorelease];
}
}
case NSFetchedResultsChangeMove:
if (newIndexPath != nil) {
NSUInteger tableSectionCount = [self.tableView numberOfSections];
NSUInteger frcSectionCount = [[controller sections] count];
if (frcSectionCount != tableSectionCount + sectionInsertCount) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:[newIndexPath section]] withRowAnimation:UITableViewRowAnimationNone];
sectionInsertCount++;
}
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath]
withRowAnimation: UITableViewRowAnimationRight];
}
else {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:[indexPath section]] withRowAnimation:UITableViewRowAnimationFade];
}
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1))) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount++;
}
break;
case NSFetchedResultsChangeDelete:
if (!((sectionIndex == 0) && ([self.tableView numberOfSections] == 1) )) {
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
sectionInsertCount--;
}
break;
case NSFetchedResultsChangeMove:
break;
case NSFetchedResultsChangeUpdate:
break;
default:
break;
}
}
You can find the fixed version of the Chapter 7 application right here.
Friday, March 26, 2010
Improved Irregular Shape UIButton
I took some of the feedback and improved the UIButton subclass from my last post. I implemented a cache for the alpha data and also incorporates changes based on Alfons Hoogervorst's modifications to my original UIImage category.
You can find the new and improved version of the irregular shaped UIButton code here.
You can find the new and improved version of the irregular shaped UIButton code here.
Irregularly Shaped UIButtons
Note: There is an improved version of the code from this blog post right here.
You probably know that UIButton allows you to select an image or background image with alpha, and it will respect the alpha. For example, if I create four images that look like this:
I can then use create custom buttons in Interface Builder using these images, and whatever is behind the transparent parts of the button will show through (assuming the button is not marked opaque. However, UIButton's hit-testing doesn't take the transparency into account, which means if you overlap these buttons in Interface Builder so they look like this, for example:
If you click here:
The default hit-testing is going to result in the green diamond button getting pressed, not the blue one. While this might be what you want some of the time, typically this won't be the behavior want. So, how do you get it to work like that? It's actually pretty easy, you just need to subclass UIButton and override the hit testing method.
But, first, we need a way to determine if a given point on an image is transparent. Unfortunately, UIImage is an opaque type without a mechanism to give us easy access to the bitmap data the way NSBitmapRepresentation does for NSImages in Cocoa. But, every UIImage instance does have a property called CGImage that gives us access to the underlying image data, and Apple has very nicely published a tech note telling how to get access to the underlying bitmap data from a CGImageRef.
Using the information in that technote, we can easily craft a category on UIImage with a method that takes a CGPoint as an argument and returns either YES or NO depending on whether the alpha value that corresponds to that point is transparent (0).
UIImage-Alpha.h
#import <UIKit/UIKit.h>
@interface UIImage(Alpha)
- (NSData *)ARGBData;
- (BOOL)isPointTransparent:(CGPoint)point;
@end
UIImage-Alpha.m
CGContextRef CreateARGBBitmapContext (CGImageRef inImage)
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
size_t pixelsWide = CGImageGetWidth(inImage);
size_t pixelsHigh = CGImageGetHeight(inImage);
bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);
colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL)
return nil;
bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
CGColorSpaceRelease( colorSpace );
return nil;
}
context = CGBitmapContextCreate (bitmapData,
pixelsWide,
pixelsHigh,
8,
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst);
if (context == NULL)
{
free (bitmapData);
fprintf (stderr, "Context not created!");
}
CGColorSpaceRelease( colorSpace );
return context;
}
@implementation UIImage(Alpha)
- (NSData *)ARGBData
{
CGContextRef cgctx = CreateARGBBitmapContext(self.CGImage);
if (cgctx == NULL)
return nil;
size_t w = CGImageGetWidth(self.CGImage);
size_t h = CGImageGetHeight(self.CGImage);
CGRect rect = {{0,0},{w,h}};
CGContextDrawImage(cgctx, rect, self.CGImage);
void *data = CGBitmapContextGetData (cgctx);
CGContextRelease(cgctx);
if (!data)
return nil;
size_t dataSize = 4 * w * h; // ARGB = 4 8-bit components
return [NSData dataWithBytes:data length:dataSize];
}
- (BOOL)isPointTransparent:(CGPoint)point
{
NSData *rawData = [self ARGBData]; // See about caching this
if (rawData == nil)
return NO;
size_t bpp = 4;
size_t bpr = self.size.width * 4;
NSUInteger index = point.x * bpp + (point.y * bpr);
char *rawDataBytes = (char *)[rawData bytes];
return rawDataBytes[index] == 0;
}
@end
Once we have the ability to tell if a particular point on an image is transparent, we can then create our own subclass of UIButton and override the hitTest:withEvent: method to do a slightly more sophisticated hit test than UIButton's. The way this works is that we need to return an instance of UIView. If the point is not a hit on this view or one of its subclasses, we return nil.. If it's a hit on a subview, we return the subview that was hit, and if it's a hit on this view, we return self.
However, we can simplify this a little because, although UIButton, inherits from UIView and can technically have subviews, it is exceedingly uncommon to do so and, in fact, Interface Builder won't allow it. So, we don't have to worry about subviews in our implementation unless we're doing something really unusual. Here's a simple subclass of UIButton that does hit-testing based on the alpha channel of the image or background image of the button, but assumes there are no subviews.
IrregularShapedButton.h
#import <UIKit/UIKit.h>
@interface IrregularShapedButton : UIButton {
}
@end
IrregularShapedButton.m
#import "IrregularShapedButton.h"
#import "UIImage-Alpha.h"
@implementation IrregularShapedButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!CGRectContainsPoint([self bounds], point))
return nil;
else
{
UIImage *displayedImage = [self imageForState:[self state]];
if (displayedImage == nil) // No image found, try for background image
displayedImage = [self backgroundImageForState:[self state]];
if (displayedImage == nil) // No image could be found, fall back to
return self;
BOOL isTransparent = [displayedImage isPointTransparent:point];
if (isTransparent)
return nil;
}
return self;
}
@end
If we change the class of the four image buttons in Interface Builder from UIImage to IrregularShapedButton, they will work as expected. You can try the code out by downloading the Xcode project. Improvements and bug-fixes are welcome.
Curiously, the documentation for hitTest:withEvent: in UIView says This method ignores views that are hidden, that have disabled user interaction, or have an alpha level less than 0.1.. In my testing, this is actually not true, though I am unsure whether it's a documentation bug or an implementation bug.
Update: My Google-Fu failed me. I did search for existing implementations and tutorials about this subject before I wrote the posting (I hate reinventing the wheel), but I failed to find Ole Begemann's implementation of this from a few months ago. It's worth checking out his implementation to see different approaches to solving the same problem. There's also some discussion in the comments about the differences in our implementations that may be of interest if you like knowing the nitty-gritty details. Plus, his diamonds are prettier than mine.
Update 2: Alfons Hoogervorst tweaked the code andshowed how you could reduce the overhead by creating an alpha-only context.
Subscribe to:
Posts (Atom)