When an iOS app restores its state – when the user switched back into the app but the operating system have killed it off because it needed the memory space – often it needs to do so while its data store haven’t been brought online. This is more true if you’re using Core Data through a
UIManagedDocument
subclass. Even when you open the document inside applicationWillFinishLaunchingWithOptions
Cocoa will commence state restoration first before the event loop has its first cycle. Thus it’s likely that the document won’t finish opening during state restoration and made its NSManagedObjectContext
ready for you to fetch your data objects to re-populate your view contents.
This means that your UI restoration code need to work without Core Data objects. Often it means that you need to save and restore data that your views are displaying at the moment – things such as button titles that changes dynamically. At the first iteration of the run loop you’ll app will be similar of a hollywood film set house – only the façade is present and nothing else behind it – until the Core Data stack is ready. You’ll need to do this to avoid displaying empty buttons when the app starts, because your app will start drawing itself just after state restoration completes and the run loop is about to have its first cycle.
Saving UIButton
titles in particular requires more work than plain labels or text fields because button labels can be attributed strings and they have four states which may be associated with their own labels. Furthermore I learned that the numberOfLines
property may need to be saved along with the title – really important if you have a multiline attributed string as a label.
So I made these two category methods on NSCoder
that works quite well. They save the titles for each state in a dictionary under a single key and read it back in, taking care not to mix up NSString
titles with their NSAttributedString
counterparts.
@interface NSCoder (UIExtras) #if TARGET_OS_IPHONE -(void) encodeButtonTitle:(UIButton*) button forKey:(NSString*) key; -(void) decodeButtonTitle:(UIButton*) button forKey:(NSString*) key; #endif @end @implementation NSCoder (UIExtras) #if TARGET_OS_IPHONE -(void) encodeButtonTitle:(UIButton*) button forKey:(NSString*) key { if(!button) { return; } NSMutableDictionary* titles = [NSMutableDictionary new]; void(^encodeState)() = ^(UIControlState state) { NSAttributedString* attributedString = [button attributedTitleForState:state]; if (attributedString) { titles[@(state)] = attributedString; } else { NSString* str = [button titleForState:state]; if (str) { titles[@(state)] = str; } } }; encodeState(UIControlStateNormal); encodeState(UIControlStateHighlighted); encodeState(UIControlStateDisabled); encodeState(UIControlStateSelected); NSDictionary* dict = @{ @"titles":titles, @"numberOfLines" : @(button.titleLabel.numberOfLines) }; [self encodeObject:dict forKey:key]; } -(void) decodeButtonTitle:(UIButton*) button forKey:(NSString*) key { if(!button) { return; } NSDictionary* dict = [self decodeObjectForKey:key]; if ([dict isKindOfClass:[NSDictionary class]]) { button.titleLabel.numberOfLines = [dict[@"numberOfLines"] integerValue]; NSDictionary* titles = dict[@"titles"]; if ([titles isKindOfClass:[NSDictionary class]]) { void(^decodeState)() = ^(UIControlState state) { id obj = titles[@(state)]; if ([obj isKindOfClass:[NSAttributedString class]]) { [button setAttributedTitle:obj forState:state]; } else if([obj isKindOfClass:[NSString class]]) { [button setTitle:obj forState:state]; } }; decodeState(UIControlStateSelected); decodeState(UIControlStateHighlighted); decodeState(UIControlStateDisabled); decodeState(UIControlStateNormal); } } } #endif @end
I’m these method as part of the Speech Timer 2.0 remake. UI restoration is one of the challenging aspects that I’ve solved recently while writing the app. Please let me know what you think!
Until next time.
0 thoughts on “Encoding UIButton Title for State Preservation and Restoration”
You must log in to post a comment.