Apple TV, AirPlay
I need to do an iOS dev presentation this weekend at a local user group, one problem to solve is to connect my iPad to their projector. I know there is iPad to VGA or HDMI cable, cost about $40-50, Apple TV seems a much better solution, AirPlay should solve the problem easily.
Hook up AppleTV to my receiver is easy, only wifi password basically. I need to turn on the iTunes Home sharing feature on my MacMini, then on AppleTV I can see that shared iTunes library. Surprised Apple requests AppleID to turn home sharing on, all info save on their server?
Watched a few trailers, Hunger Games, AV quality is awesome. My daughter quickly took it over and starting watch youtube on AppleTV.
One annoying limitation for Youtube on AppleTV is no Chinese input, so can’t search Youtube with Chinese titles. Solution is login with youtube id, then can play youtube playlist created on somewhere else.
Remote control for AppleTV is cute, but too small for my hand, download the app called remote for my iPad, I can then control AppleTV on iPad. It’s good for playing music without turn on real TV.
About AirPlay, not many apps support this feature yet. Photos is one of them, but not Videos! Some people end up with converting their powerpoint file to separated images to be able to play on project through AirPlay.
Luckily, I have a iPad2. Turn on AirPlay mirroring feature on it and everything shows on iPad can be outputted to TV. Cool!
The app I developed, including iClip and maodou, are using iOS native media player, AirPlay doesn’t have any problem. But one of the app I purchased called OPlayer, AirPlay only works for the sound. no video. I had to back to another app called streamtome to play those non-h264 videos on my computer.
Sqlite and CoreData in iOS, repository pattern
I haven’t see too many articles talking about iOS Core-Data development yet, so I documented something important here for future reference.
- No need to create ID property as identity column for your model, in fact, id is a reserved keyword for cocoa, IDE will looks weird for id property in model. core-data will generate a few extra columns in your model table, including ZID, Z…, all have a Z prefix.
- All number like fields will be mapped to NSNumber.
- Create app with core-data support, in XCode4, single view app template doesn’t have this core-data support option, try create another project in different project template, then copy the generated core-data code from appDelegate into your project.
- If you have your own existing database which will be installed with app bundle, you need to manually copy it from bundle to document folder to make it writable.
- New object needs to save into database later should created by insertNewObjectForEntityForName.
- Hold the managedObjectContext till the app terminate, that’s why core-data code were in appDeledate, if move into you own class, create retain property.
Personnly, I like to implement repository pattern for all coredata operations, including initialize. My sample repository looks like this:
@interface iCWOccurrenceRepository()
- (NSMutableArray *)retrieveAllOccurrencesForCity:(NSString*)city sinceDate:(NSDate*)startDate;
@end
@implementation iCWOccurrenceRepository
@synthesize managedObjectContext = __managedObjectContext;
@synthesize managedObjectModel = __managedObjectModel;
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
NSString * DBNAME = @"mydb.sqlite";
- (void)saveANew:(NSArray*) line{
NSManagedObjectContext * context = self.managedObjectContext;
NSManagedObject *newOccurrence = [NSEntityDescription
insertNewObjectForEntityForName:@"Occurrence"
inManagedObjectContext:context];
Occurrence* oToSave = ((Occurrence*)newOccurrence);
oToSave.id = [NSNumber numberWithInt:[[line objectAtIndex:0] intValue]];
oToSave.city = [line objectAtIndex:1];
//...
[self saveContext];
}
- (void)saveContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = __managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
#pragma mark - Core Data stack
/**
Returns the managed object context for the application.
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
*/
- (NSManagedObjectContext *)managedObjectContext
{
if (__managedObjectContext != nil)
{
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
__managedObjectContext = [[NSManagedObjectContext alloc] init];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return __managedObjectContext;
}
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created from the application's model.
*/
- (NSManagedObjectModel *)managedObjectModel
{
if (__managedObjectModel != nil)
{
return __managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"iCW" withExtension:@"momd"];
__managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return __managedObjectModel;
}
/**
Returns the persistent store coordinator for the application.
If the coordinator doesn't already exist, it is created and the application's store added to it.
*/
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (__persistentStoreCoordinator != nil)
{
return __persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:DBNAME];
NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
{
[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return __persistentStoreCoordinator;
}
#pragma mark - Application's Documents directory
/**
Returns the URL to the application's Documents directory.
*/
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
-(void)createEditableCopyOfDatabaseIfNeeded {
//
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectory = [self applicationDocumentsDirectory];
NSString *writeableDBPath = [[documentDirectory
URLByAppendingPathComponent:DBNAME]
path];
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:DBNAME];
BOOL defaultDbExists = [fileManager fileExistsAtPath:defaultDBPath];
BOOL dbExistsInDocFolder = [fileManager fileExistsAtPath:writeableDBPath];
if (!dbExistsInDocFolder && defaultDbExists) {
NSError *error;
BOOL success = [fileManager copyItemAtPath:defaultDBPath toPath:writeableDBPath error:&error];
if (!success) {
NSAssert1(0, @"Failed to create writable database file with message '%@'.",
[error localizedDescription]);
}
}
}
- (NSMutableArray *)retrieveAllOccurrencesForCity:(NSString*)city sinceDate:(NSDate*)startDate{
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Occurrence"
inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"id" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc]
initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
NSPredicate *bPredicate =
[NSPredicate predicateWithFormat:@"city == %@ and date > %@", city, startDate];
[request setPredicate:bPredicate];
NSError *error;
NSMutableArray *fetchResults = [[managedObjectContext executeFetchRequest:request
error:&error] mutableCopy] ;
if (fetchResults == nil) {
// handle the error
}
return fetchResults;
}
@end
Again, this repository class needs to be a strong retain property in caller.
Lookup in db or in code (enum)?
Maybe this is a solved problem already, but I couldn’t figure it out until today. With massive usage of NHibernate, lookup in db or in code always appear as a puzzle to me. For example, given a lookup dictionary like mime type: text/plain, text/html, image/gif and other stuff. It make sense to save all those data into a table then enable foreign key on all referenced tables. But, in code, those mime type id itself will appear like magic number, I don’t likes this.
So, creating an enum contains all items solve the magic number issue, partly, and introduce another sync concern, what if data in db and enum in code out of sync?
This post demos a brilliant idea of creating enum on the fly, but mime value has slash in it, this solution doesn’t work. Also, wrapper class around enum seems unnecessary.
Based on the suggestion from this post. here comes my solution,
public enum MimeType
{
[Description("application/octet-stream")] ApplicationOctetStream,
[Description("text/plain")] TextPlain = 1,
[Description("text/html")] TextHtml,
[Description("application/pdf")] ApplicationPdf,
[Description("application/vnd.ms-excel")] ApplicationVndMsExcel,
[Description("image/gif")] ImageGif,
[Description("image/jpeg")] ImageJpeg,
[Description("application/rtf")] ApplicationRtf,
[Description("application/zip")] ApplicationZip,
[Description("application/msword")] ApplicationMsword,
[Description("application/mspowerpoint")] ApplicationMspowerpoint
}
/// <summary>
/// Extension methods container for enum used to check synchronization between enum in code and data in db.
/// see usage in should_fetch_all_mime_types() of EmailQueueRepositoryTest class
/// </summary>
public static class MimeTypeEx
{
public static MimeType ToMimeTypeValue(this string value)
{
foreach (FieldInfo fi in typeof (MimeType).GetFields())
{
if (!fi.IsStatic)
{
continue;
}
object[] attrs = fi.GetCustomAttributes(typeof (DescriptionAttribute), false);
if (attrs == null || attrs.Length <= 0)
{
continue;
}
var descr = (DescriptionAttribute) attrs[0];
if (0 == string.Compare(value.Trim(), descr.Description))
// in case varchar type. trim it before compare.
{
return (MimeType) fi.GetValue(null);
}
}
throw new InvalidDataException(value);
//return MimeType.Unknown;
}
public static string ToMimeTypeString(this MimeType value)
{
foreach (FieldInfo fi in typeof (MimeType).GetFields())
{
if (!fi.IsStatic || (MimeType) fi.GetValue(null) != value)
{
continue;
}
object[] attrs = fi.GetCustomAttributes(typeof (DescriptionAttribute), false);
if (attrs == null || attrs.Length <= 0)
{
continue;
}
var descr = (DescriptionAttribute) attrs[0];
return descr.Description;
}
return string.Empty;
}
}
/// <summary>
/// Used to read mime type from db, then we can check synchronization with defined enum mime type.
/// </summary>
public class MimeTypeInDb
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual bool BinaryInd { get; set; }
}
Test code, similar functionality should be done in app start or somewhere static ctor to ensure the synchronization.
[Test]
public void should_fetch_all_mime_types()
{
var results = _repository.FetchAll<MimeTypeInDb>();
Assert.That(results.Count(), Is.GreaterThan(0));
foreach (MimeTypeInDb mimeTypeInDb in results)
{
Console.WriteLine(mimeTypeInDb.Name.ToMimeTypeValue()); // will throw invalid data exception if enum not found.
}
foreach (MimeType mimeType in Enum.GetValues(typeof(MimeType)))
{
Console.WriteLine(mimeType.ToMimeTypeString());
if (! results.Any(x => x.Name.Trim() == mimeType.ToMimeTypeString()))
{
Assert.Fail(mimeType.ToMimeTypeString() +" not found in db.");
}
}
}
About mapping the enum to int in FluentNHibernate:
Map(x => x.MimeType).Column("mime_type_id").CustomType<MimeType>();
Note: using CustomType<int>() also works, but it will cause extra update problem for reading query. NH is trying to set column to enum string value.
Cloud means surprise
I have been looking for an opportunity to work on queue base project after finishing Udi Dahan’s Advanced distributed system design course. Because most of my projects are running on Linux OS where NSerivceBus is not applicable, using Amazon SQS became a affordable work around for now.
Problem to solve
My iClip iOS app recently encountered unexpected popular requests for unknown reason (listed somewhere on an iOS app category site?). Even I only grant every new client only 3 free requests, with the huge amount of daily downloads, my server becomes slow and unstable gradually.
I could keep bumping up the memory on server, but I don’t like this solution at all. First, server hosting bill will go up dramatically; second, hosting server memory still has a max limitation of 4GB. Someday the client requests will eventually break this ceiling, then what?
Solution
I decided to move my request model to queue based architecture based on Amazon’s SQS service. Rewriting app didn’t take very long. After a week of production experience, here are some gotcha I’ve learned.
- Message won’t be immediately available after send. Amazon actually state this delay will be up to 60 seconds on SQS admin console.
- Deleted message might still be visible in queue. Explanation from Amazon is:
It is possible you will receive a message even after you have deleted it. This might happen on rare occasions if one of the servers storing a copy of the message is unavailable when you request to delete the message. The copy remains on the server and might be returned to you again on a subsequent receive request. You should create your system to be idempotent so that receiving a particular message more than once is not a problem.- For the similar reason as above, queue processor should really deal with duplicated request very carefully. For me, I use sendtimestamp as the identity of message.
- Receiving message must set the visibility time out to a value greater than zero, to allow this message to be deleted successfully afterwards. (To be confirmed)
Re-configure dreamhost evniorment to support Amazon WebService
I was trying to make my dreamhost to support Amazon WebService (aws), the first problem I encountered is, missing libxslt.
Following the instruction here, I managed to install libxslt to my custom location.
Watch out, the link on http://xmlsoft.org/XSLT/downloads.html is actually pointing to libxml2 which doesn’t include libxslt. You want to find the correct link from ftp://xmlsoft.org/libxslt/
The next problem is openssl support missing from ruby. I had to re-configure ruby to make it openssl ready. Instruction can be found here.
All aws samples passed, then.
With one minor issue left, I got warning when running ruby:
Invalid gemspec in [/home/me/.gems/specifications/openssl-extensions-1.2.0.gemspec]: invalid date format in specification: “2011-11-03 00:00:00.000000000Z”
Hope I can figure this out later.
- filter type set to log4net.Filter.StringMatchFilter
- each filter section can only have one stringToMatch element
- end with a “log4net.Filter.DenyAllFilter” filter to switch from the default “accept all unless instructed otherwise” filtering behavior to a “deny all unless instructed otherwise” behavior.
<?xml version="1.0" encoding="utf-8"?>
<log4net debug="false">
<appender name="SmtpAppender_WebTeam" type="log4net.Appender.SmtpAppender">
<to value="goodguy@me.com" />
<from value="log4net@me.ca" />
<subject value="!!!Web Exception happened!!!" />
<smtpHost value="mail.server.ds" />
<bufferSize value="512" />
<lossy value="true" />
<evaluator type="log4net.Core.LevelEvaluator">
<threshold value="ERROR"/>
</evaluator>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%newline%date [%thread] %-5level %logger [%property{NDC}] - %message%newline%newline%newline" />
</layout>
<filter type="log4net.Filter.StringMatchFilter">
<stringToMatch value="RequestManagement" />
</filter>
<filter type="log4net.Filter.StringMatchFilter">
<stringToMatch value="VineOnLine" />
</filter>
<filter type="log4net.Filter.DenyAllFilter" />
</appender>
<appender name="SmtpAppender_VSE" type="log4net.Appender.SmtpAppender">
<to value="nice@me.ca" />
<from value="log4net@mr.ca" />
<subject value="!!!VSE Exception happened!!!" />
<smtpHost value="mail.server.ds" />
<bufferSize value="512" />
<lossy value="true" />
<evaluator type="log4net.Core.LevelEvaluator">
<threshold value="ERROR"/>
</evaluator>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%newline%date [%thread] %-5level %logger [%property{NDC}] - %message%newline%newline%newline" />
</layout>
<filter type="log4net.Filter.StringMatchFilter">
<stringToMatch value="VSE" />
</filter>
<filter type="log4net.Filter.DenyAllFilter" />
</appender>
<root>
<level value="INFO" />
<appender-ref ref="SmtpAppender_WebTeam" />
<appender-ref ref="SmtpAppender_VSE" />
</root>
<logger name="NHibernate">
<level value="ERROR"/>
</logger>
</log4net>
NHibernate Session.Query ignore Fetch Join?
We set up child collection relationship to Fetch as Join in mapping, using fluentNHibernate:
HasMany( x => Details ).Fetch.Join();
It works fine at least for Session.Get<T>(id), we can see only one joined query instead of two separated ones.
But it seems Session.Query<T>() keeps ignoring this join fetch setting, we always got two separated selects when using Session.Query<T>. A Bug?





