Andy Heydon

The Samsung Galaxy S4 Release Party

I’ve just watched last night’s Samsung Galaxy S4 release event and while it was shockingly corny with sexist undertones, the one thing I found really interesting is what wasn’t said. Google was not mentioned a single time, and there was only a single passing reference to Android, and that in relation to a security feature.

Samsung has positioned itself as the smartphone company with largest market share and second largest profits, and is now using a boat load of branded, “S whatever”, mainly software, features to differentiate themselves not only from the other Android vendors but from it’s heritage. As Samsung progresses along this trajectory and becomes more and more confident then there could come a point when Google’s control over Android is snapped. If the dominant Android vendor reaches that point and decides it can pull an Amazon by living off a forked and heavily skinned version of Android along with a proprietary store then that leaves Google in a very tricky position. Do they let the one successful company in the ecosystem take all the glory without getting any credit, or do they try to reign in the skinning and supplementary features in an attempt to retain a measure of control and relevance?

The waters are further muddied this summer when Samsung will release a Tizen based smartphone. Tizen is another open source operating system based around Linux and WebKit with which Samsung has been involved for years but gained an important ally when Intel signed up — Intel has famously been shut out of the mobile generation and is eager to make up for lost ground. If Samsung is able to focus customer attention on the “S features” and can offer them on all their mobile platforms then the operating system becomes fungible and ultimately irrelevant, leaving Samsung a big winner and in complete control of it’s own destiny while Google is left supporting a bunch of loss-making phone companies and cheap Chinese knockoffs.

Advertisement

Infinitely scrolling tables

One of the reasons I started my latest app, MealSchedule, was because I don’t care for presenting dates in blocks such as a week or a month. I don’t like that as you reach the end of the block a large proportion of the screen contains irrelevant historical data and I don’t like that it is cumbersome to compare items on either side of the block’s boundary. Time is continuous, therefore its display should be unbroken. Just because a week is nice and convenient for having one of each kind of day, you ignore that not every-one’s week starts on a Monday or a Sunday.

To translation a continuous time display to an app, you really need to use a vertical scroll as this is by far the most natural method for presenting an infinite amount of data. So the first challenge for my new app was to develop a way to handle and present an unlimited scrolling of dates.

The simplest option, of course, is to use a UITableView and return a very large number of rows. You can use scrollToRowAtIndexPath: to position today as the initial row, somewhere in the middle of the table. This works fairly well and is very easy to implement provided, and this is a big constraint, that each row is always the same height, indeed you do not want to implement tableView:heightForRowAtIndexPath: at all. iOS needs the height of each row to calculate the offsets of each cell within the scroll view of a table. When you call scrollToRowAtIndexPath: for example, iOS must determine the height of each preceding row, if that height is variable then the costs of calculating the offsets become untenable as soon as the table has more than a couple of hundred rows. With a fixed height, iOS will simply do the math to calculate the offset for a particular row.

But MealSchedule was going to have variable height content, so the simplest solution is off the table (pun intended!) But maybe we could be smarter and use a smaller number of rows as a window on to the underlying set of dates? Every time the user scrolls I could just change the dates that the visible rows represent.

A UITableView is a subclass of UIScrollView, so by implementing scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: from the UITableView delegate – a table’s delegate serves up all of the UIScrollViewDelegate methods, I can use a quick sleight of hand to update the dates for the visible rows and reload the table. You have to implement both methods because either will be called depending on how the user scrolls. This pattern is a huge improvement over the first option, but suffers from a jerky appearance because with only a certain number of rows in the table there is an end point of the scrolling and there is a delay before I can update them.

While this second option was better there is still too much lag because of UITableView overhead in calculating row size. So the third approach is to strip out the overhead. UITableView is a complex class that takes care of a lot of situations, but I didn’t necessarily need all of that. What if I just had a UIScrollView and used by own UIViews to represent each day?

This proves to be a great solution. I could quickly add and remove subviews from the start and end of the content when the UIScrollView was scrolled and I could have variable height rows because I could control the number of times each row had to be recalculated. layoutSubviews is called every time a UIScrollView is scrolled, so I can quickly check to see if we are close to one end or the other, and if we are, remove the day subview from the opposite end, add in a new day and simply adjust the origins of the remaining subviews.

I choose to have a few extra days on either end of the visible set to allow for smoother scrolling. There is a function in the app to move to another date, and if that date is within the current set I can perform a nice animated scroll. But if the date is outside of the range, then I have to recalculate the entire set and there is no animated scroll. Most date changes are going to be by a week, so that is how many extra days I have available, just in case.

Here is the code in my UIScrollView subclass that handles the day subview creation and the scrolling (I have removed parts that are not relevant to this discussion.). The day subview is a UIView subclass called MealsDateView, the details of which are also irrelevant.

@interface MealPlanScrollView () {
NSMutableArray *_dayViews;
NSInteger _numberOfDays;
NSInteger _viewFirstDateNum;
NSInteger _viewLastDateNum;
NSInteger _todayDateNum;
NSInteger _maxNumberOfDays;
}
- (void)buildDayViews;
- (MealsDateView *)insertDayAtIndex:(NSInteger)index;
- (CGFloat)deleteDay:(MealsDateView *)day;
- (void)adjustDayOriginsBy:(CGFloat)distance startingAt:(NSInteger)start;
- (MealsDateView *)findViewForDateNum:(NSInteger)dateNum;
- (void)setTodayRow;
@end
@implementation MealPlanScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Base the number of days on the device size
// use a week either side to allow for smooth scrolling when moving by 7 days
_numberOfDays = roundf([UIScreen portraitHeight] / DEFAULT_ROW_HEIGHT) + 14;
_maxNumberOfDays = [NSDate maxNumberOfDates];
[self setContentSize:CGSizeMake(CGRectGetWidth(frame), 0)];
[self setDelegate:self];
[self setTodayRow];
_viewFirstDateNum = _todayDateNum - _numberOfDays/2;
_viewLastDateNum = _todayDateNum + _numberOfDays/2;
// Build the day views
_dayViews = [[NSMutableArray alloc] init];
[self buildDayViews];
[self setYear];
MealsDateView *firstVisible = _dayViews[(_numberOfDays/2)-1];
[self setContentOffset:CGPointMake(0, CGRectGetMinY([firstVisible frame]))];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// Adjust the scroll offset if gone too far one way or the other
CGPoint currentOffset = [self contentOffset];
currentOffset.x = 0;
CGFloat contentHeight = [self contentSize].height;
CGFloat centerOffsetY = (contentHeight - CGRectGetHeight([self bounds])) / 2.0f;
CGFloat distanceFromCenter = currentOffset.y - centerOffsetY;
CGFloat buffer = DEFAULT_ROW_HEIGHT;
if (fabs(distanceFromCenter) > buffer) {
if (distanceFromCenter < 0) {
// move from bottom to top
while (fabs(distanceFromCenter) > buffer) {
if (_viewFirstDateNum == 0) {
break;
}
_viewFirstDateNum--;
_viewLastDateNum--;
[self deleteDay:[_dayViews lastObject]];
MealsDateView *firstDay = [self insertDayAtIndex:0];
CGFloat h = CGRectGetHeight([firstDay frame]);
currentOffset.y += h;
[self adjustDayOriginsBy:h startingAt:1];
distanceFromCenter += h;
if (distanceFromCenter >= 0) {
break;
}
}
} else {
// move from top to bottom
while (distanceFromCenter > buffer) {
if ((_viewLastDateNum-1) == _maxNumberOfDays) {
break;
}
_viewFirstDateNum++;
_viewLastDateNum++;
CGFloat h = [self deleteDay:_dayViews[0]];
[self insertDayAtIndex:[_dayViews count]];
[self adjustDayOriginsBy:-1*h startingAt:0];
currentOffset.y -= h;
distanceFromCenter -= h;
if (distanceFromCenter <= 0) {
break;
}
}
}
}
[self setContentOffset:currentOffset];
}
#pragma mark - Public
- (void)changeRowHeightOfDate:(NSDate *)date by:(CGFloat)delta {
NSInteger dateNum = [date daysSinceStart];
MealsDateView *day = [self findViewForDateNum:dateNum];
if (!day) {
return;
}
CGFloat oldHeight = CGRectGetHeight([day frame]);
// Adjust the size of the views
CGFloat newHeight = oldHeight + delta;
if (newHeight != oldHeight) {
CGFloat diff = newHeight - oldHeight;
CGSize s = [self contentSize];
s.height += diff;
[self setContentSize:s];
[self adjustDayOriginsBy:diff startingAt:(dateNum-_viewFirstDateNum+1)];
}
}
- (void)layoutMeals {
CGRect f = [self frame];
f.size.width = [UIScreen width];
f.size.height = [UIScreen height]-[UIScreen statusBarHeight]-f.origin.y;
[self setFrame:f];
CGFloat y = 0;
for (MealsDateView *day in _dayViews) {
CGRect df = [day frame];
if (CGRectGetMinX(df) != y) {
df.origin.y = y;
[day setFrame:df];
}
[day layoutMeals];
y += CGRectGetHeight([day frame]);
}
CGSize s = [self contentSize];
s.height = y;
[self setContentSize:s];
}
#pragma mark - Meal actions
- (void)scrollToDate:(NSDate *)date inRect:(CGRect)rect {
BOOL reload = NO;
NSInteger newDateNum = [date daysSinceStart];
if ((newDateNum <= _viewFirstDateNum) || (newDateNum >= _viewLastDateNum)) {
_viewFirstDateNum = newDateNum - _numberOfDays/2;
_viewLastDateNum = newDateNum + _numberOfDays/2;
if (_viewFirstDateNum < 0) {
_viewFirstDateNum = 0;
_viewLastDateNum = _viewFirstDateNum + _numberOfDays;
}
if (_viewLastDateNum > [NSDate maxNumberOfDates]) {
_viewLastDateNum = [NSDate maxNumberOfDates];
_viewFirstDateNum = _viewLastDateNum - _numberOfDays;
}
reload = YES;
}
if (reload) {
[self buildDayViews];
}
NSInteger newRow = newDateNum - _viewFirstDateNum;
if (newRow < 0) {
newRow = 0;
}
if (newRow >= [_dayViews count]) {
newRow = [_dayViews count] - 1;
}
MealsDateView *day = _dayViews[newRow];
CGRect f = [self frame];
CGRect df = [day frame];
CGPoint offset = [self contentOffset];
// rect is relative to the screen
offset.y = df.origin.y - (rect.origin.y - f.origin.y);
if (offset.y < 0) {
offset.y = 0;
}
[CATransaction begin];
[self setContentOffset:offset animated:!reload];
[CATransaction commit];
}
- (void)scrollByDistance:(CGFloat)distance {
CGPoint offset = [self contentOffset];
offset.y -= distance;
[UIView animateWithDuration:IOS_ANIMATION_DURATION
delay:0.0f
options:IOS_ANIMATION_CURVE
animations:^{
[self setContentOffset:offset animated:NO];
}
completion:^(BOOL finished) {
if (finished) {
}
}];
}
#pragma mark - Local
- (void)buildDayViews {
for (MealsDateView *day in _dayViews) {
[day removeFromSuperview];
}
[_dayViews removeAllObjects];
CGSize s = [self contentSize];
s.height = 0;
[self setContentSize:s];
for (int i=0; i<_numberOfDays; i++) {
[self insertDayAtIndex:i];
}
}
- (MealsDateView *)insertDayAtIndex:(NSInteger)index {
NSInteger dayNum = (_viewFirstDateNum + index);
NSDate *thisDate = [NSDate dateSinceStartForDayNumber:dayNum];
CGFloat y = 0;
if (index > 0) {
UIView *prevDay = _dayViews[(index-1)];
y = CGRectGetMaxY([prevDay frame]);
}
DateRowType rowType = DateRowTypeMiddle;
if (dayNum == 0) {
rowType = DateRowTypeFirst;
} else if (dayNum == _maxNumberOfDays) {
rowType = DateRowTypeLast;
}
MealsDateView *day = [[MealsDateView alloc] initWithDate:thisDate rowType:rowType];
CGRect f = [day frame];
f.origin.y = y;
[day setFrame:f];
[_dayViews insertObject:day atIndex:index];
[self insertSubview:day atIndex:0];
CGSize s = [self contentSize];
s.height += CGRectGetHeight([day frame]);
[self setContentSize:s];
return day;
}
- (CGFloat)deleteDay:(MealsDateView *)day {
CGRect f = [day frame];
CGSize s = [self contentSize];
s.height -= CGRectGetHeight(f);
[self setContentSize:s];
[day removeFromSuperview];
[_dayViews removeObject:day];
return CGRectGetHeight(f);
}
- (void)adjustDayOriginsBy:(CGFloat)distance startingAt:(NSInteger)start {
for (int i=start; i<[_dayViews count]; i++) {
MealsDateView *day = (MealsDateView *)_dayViews[i];
CGRect f = [day frame];
f.origin.y += distance;
[day setFrame:f];
}
}
- (MealsDateView *)findViewForDateNum:(NSInteger)dateNum {
if ((dateNum < _viewFirstDateNum) || (dateNum > _viewLastDateNum)) {
return nil;
}
NSInteger index = dateNum - _viewFirstDateNum;
if ((index >= 0) && (index < [_dayViews count])) {
return _dayViews[index];
} else {
return nil;
}
}
#pragma mark - Date/time methods
- (void)setTodayRow {
_todayDateNum = [[NSDate today] daysSinceStart];
}
view raw gistfile1.m hosted with ❤ by GitHub

I have a category to extend NSDate which provides methods to convert between a full date and an integer as it is easier to work with a simple scalar in managing the set of days.

The 50 Shades of Apple Crazy

If you’ve been reading any of the news and commentary about Apple the past couple of weeks then you could be mistaken for thinking that the company’s demise is imminent. The stock price is falling, orders for components are being cut, and people are offering up their thoughts on what the company needs to do to revive its brand.

Yet Apple announced last quarter revenues of $54.5 billion with a net profit of $13.1 billion, both of which are company records. Indeed the only company to have ever posted larger numbers was Exxon and they had the happy coincidence of high oil prices to juice their figures. But the great crime in Apple’s revenue was that growth is not increasing – Apple is still growing, just not at an increasing pace, a second order offense. I’m sure that there is not a CEO in the world that wouldn’t swap places with Tim Cook to announce such disappointing figures.

Now some of this Apple woe is self inflicted. Apple is a secretive company and only comes up for air at the quarterly results calls and for two or three product announcements a year. The rest of the year is pretty much silent and so journalists and analysts are reduced to scanning Chinese language rumor sites and making guesses based on competitors’ technology. But since the genie was let out of the bottle in 2007 for the original iPhone, Apple has to work increasingly harder to impress and unless then come up radical innovation then there is always going to be a chorus of ho-hums.

Much is made of Apple’s decreasing market share, even though they sold 75 million iOS devices in the last quarter. But interestingly, AT&T announced that 84% of smartphone activations in the latest quarter where for iPhones, and for Verizon iPhones accounted for 63% of all their smartphones sold. So, in the US at least, Apple’s market share is heading in an upwards direction.

Another thing about these 75 million devices is that supply of the iPhone 4 and iPad Mini was constrained for the entire quarter and the iPhone 5 for much of the quarter. So Apple could have sold many more devices if they had just been able to build them quicker. I’m sure every company would love to have problems like that. These supply constraints help to put a lie to many of the proposed solutions such as producing cheaper phones and the iPad Mini is too expensive.

Let’s consider the iPad Mini. Economic theory states that if supply exceeds demand then you increase the price, hence scaling back demand, to bring the relationship back into equilibrium. So actually Apple is leaving money on the table by selling the iPad Mini at $329, but when the product was launched, it was roundly derided as being too expensive. I guess the market doesn’t listen to the analysts.

The iPhone 4 is Apple’s cheap phone, in the US carriers sell it for $0 with a data contract. It doesn’t come much cheaper than $0 and while it doesn’t have all the processing power of an iPhone 5, it is not that compromised and can certainly run the latest version of iOS and the vast majority of apps without any difficulty. If the iPhone 4 was supply constrained for the entire quarter then cost conscious customers are certainly snapping them up. If Apple is having difficulty building all the devices it could sell (and maybe with their large capital expenditures they are moving to alleviate that problem) then the company doesn’t have much incentive to increase the product portfolio to chase down even more market segments.

Apple’s supply chain is very large and very complicated, but they have great economies of scale by restricting the number of products. Diversifying that portfolio with different screen sizes and form factors, complicates the supply chain and introduces unnecessary risks to the manufacturing cycle. Apple appears to be a company that keeps things simple to minimize risk. A reduced product mix is also beneficial to customers as a lot of redundant decisions are removed from the purchasing process. The more variables you have in the product range, the more the paralysis over decisions that are often difficult to evaluate. When you are selling 10 items every single second, and your stores are crammed full with people, you hardly want them agonizing over several perfectly good devices. More choice is not always good.

Every-one, outside of Apple, has suddenly developed these outsized expectations for the company, that it should be innovating every time it turns around. But in its 36 year existence it has introduced three original and revolutionary products – the Mac, the iPod and the iPhone. Everything else has been an iteration on those devices, which it is very good at. But this concentration on iteration is diametrically opposite to the Japanese model of throwing a bunch of stuff at a wall and seeing what sticks (and for generating journalistic excitement). Apple has built a readily identifiable brand and they are not going to cheapen it for the sake of a few extra dollars.

I don’t think Apple feels the pressure to change its approach because the market is constantly providing validation by record sales, and why should it?

Upgrading the delete confirmation button

Standard delete confirmationiOS has a nice pattern for deleting a row in a table, either tap a minus symbol in a red circle or swipe your finger along the row and a delete button slides in from the right, tap the button to confirm the action or tap elsewhere to cancel. The only problem is that if you have a different look and style than the default then the delete button looks out of place.

I encountered this recently and decided to see if I could replace the delete button with one more in keeping with the rest of the graphic style in my MealSchedule app.

Searching for examples of what other people might have done led to a couple of general approaches. The first was to subclass UITableViewCell to get the ability to override the willTransitionToState: method that is invoked as a cell moves through various editing states, and the second was to walk through the view hierarchy of the newly animated delete button and modify it. There’s nothing wrong with first suggestion because it utilizes a standard API, but the second involves testing against internal class names and assuming a particular view hierarchy, neither of which have any place in any app, regardless of whether you are submitting it to the AppStore or not. Working against an API or utilizing ordinarily hidden features is a bad smell and a strong indication that you are doing something wrong.

So the first step to having a nice delete confirmation button is to subclass UITableViewCell and override willTransitionToState:. The method is called with a bit mask of the new states. We are interested in the situation where the delete confirmation button is about to be displayed.

- (void)willTransitionToState:(UITableViewCellStateMask)state {
if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableViewCellStateShowingDeleteConfirmationMask) {
} else {
[super willTransitionToState:state];
}
}
view raw gistfile1.m hosted with ❤ by GitHub

The default transition will create the shiny delete button and animate it into place. We don’t want to do that because we want to create our own button, but we will leave the other transitions to their default for the time being, so we are careful as to when we invoke super.

The next step is to create the new button and animate it into place. MealSchedule has a UIButton subclass named MPButton that encapsulates my alternative presentation, so let’s extend the transition to include that.

- (void)willTransitionToState:(UITableViewCellStateMask)state {
CGRect f;
if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableViewCellStateShowingDeleteConfirmationMask) {
if (!_deleteButton) {
_deleteButton = [MPButton buttonWithType:MPButtonTypeAlert withTitle:NSLocalizedString(@"Delete",@"Delete")];
[[_deleteButton titleLabel] setFont:[[UIFont buttonFont] changeFontSizeBy:-2]];
[_deleteButton addTarget:self action:@selector(delete:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_deleteButton];
}
f = RBRectCenterPositionY([_deleteButton frame], CGRectGetHeight([self frame]));
f.origin.x = CGRectGetWidth([self frame]);
[_deleteButton setFrame:f];
f.origin.x = CGRectGetMaxX([self frame]) - [UIScreen tableContentPadding] - f.size.width;
_isDeleting = NO;
} else {
[super willTransitionToState:state];
if (_deleteButton) {
f = [_deleteButton frame];
if (_isDeleting) {
alpha = 0.0f;
} else {
f.origin.x = CGRectGetWidth([self frame]);
}
}
}
if (_deleteButton) {
[UIView animateWithDuration:0.3f
animations:^{
[_deleteButton setFrame:f];
[_deleteButton setAlpha:alpha];
}];
}
}
view raw gistfile1.m hosted with ❤ by GitHub

I use an instance variable to hold a reference to the delete button. I don’t strictly need to do this – I could use a tag and viewWithTag: instead, but the default implementation of the delete confirmation button uses tags within its hierarchy and so keeping a reference is safer than a tag by avoiding any unintended side effects from the base classes looking for views. The RBRectCenterPositionY is a simple function I use to center a CGRect within a particular height. I also use a category on UIScreen to return various numbers, such as the standard 10 pixels that are added as padding to a table’s content.

Note that when the cell transitions out of the delete confirmation state we want to make sure the delete button is moved out of the way, though if the cell is being deleted (as flagged by the _isDeleting variable) when just fade the button to invisible rather than move it to the right. This works as a smoother effect during the actual delete, which we will see in a minute, but for now you can tap the red minus to animate the delete button in, and tap it again to animate the button away.

The button is worthless unless we can actually perform a delete so we need to provide a method for the action we specified in the button creation.

- (void)delete:(id)sender {
_isDeleting = YES;
[self commitEdit:UITableViewCellEditingStyleDelete];
}
- (void)commitEdit:(UITableViewCellEditingStyle)editStyle {
[self willTransitionToState:UITableViewCellStateShowingEditControlMask];
if ([[_table dataSource] respondsToSelector:@selector(tableView:commitEditingStyle:forRowAtIndexPath:)]) {
NSIndexPath *ip = [_table indexPathForRowAtPoint:[self center]];
[[_table dataSource] tableView:_table commitEditingStyle:editStyle forRowAtIndexPath:ip];
}
[self didTransitionToState:UITableViewCellStateShowingEditControlMask];
}
view raw gistfile1.m hosted with ❤ by GitHub

When the delete button is tapped, it will invoke the delete: method that in turn invokes another local method named commitEdit:. As my button is a complete replacement for the standard button then we need to invoke the same transitions that would normally occur. In this particular situation, my table is always in edit mode so we will always be transitioning back to the UITableViewCellStateShowingEditControlMask state.

The table’s datasource is the class that handles the delete so we need to invoke the standard tableView:commitEditingStyle:forRowAtIndexPath: method. The only slight problem here is that we need to know our current index path, and that is not available to us from the perspective of our UITableViewCell world. Unfortunately neither is the knowledge of the containing table. The table is actually a piece of private information in the base UITableViewCell class, but is not exposed to subclasses. Therefore we need to create a property, named table here, to pass in the table reference when we create an instance of our cell. Note that you cannot call this property tableView because that will conflict with the private reference!

The final step is to handle the cancelation of the delete by tapping elsewhere in the table. The easiest way of doing that is to set up a UITapGestureRecognizer on the table.

- (void)willTransitionToState:(UITableViewCellStateMask)state {
if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableViewCellStateShowingDeleteConfirmationMask) {
// ...
_showingConfirmation = YES;
_cancelDeleteGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(deleteConfirmation:)];
[_table addGestureRecognizer:_cancelDeleteGesture];
} else {
// ...
}
// ...
}
- (void)didTransitionToState:(UITableViewCellStateMask)state {
if (_showingConfirmation) {
if ((state & UITableViewCellStateShowingDeleteConfirmationMask) != UITableViewCellStateShowingDeleteConfirmationMask) {
_showingConfirmation = NO;
if (_cancelDeleteGesture) {
[_table removeGestureRecognizer:_cancelDeleteGesture];
_cancelDeleteGesture = nil;
}
}
}
[super didTransitionToState:state];
}
// In iOS5 the tap gesture is fired before the touch on the delete button, even though the highlight happens
// In iOS6 the touch on the button fires before the tap gesture
- (void)deleteConfirmation:(UITapGestureRecognizer *)sender {
if ([sender state] == UIGestureRecognizerStateEnded) {
UITableViewCellEditingStyle editStyle = UITableViewCellEditingStyleNone;
// Determine if we actually tapped on the delete button
CGPoint curLocation = [sender locationInView:self];
if (CGRectContainsPoint([_deleteButton frame], curLocation)) {
_isDeleting = YES;
editStyle = UITableViewCellEditingStyleDelete;
}
[self commitEdit:editStyle];
}
}
view raw gistfile1.m hosted with ❤ by GitHub

The gesture recognizer is created when we transition into the delete confirmation state, and removed when we have transition out of the confirmation.

One small wrinkle that I discovered here was a difference between iOS5 and iOS6. In the later SDK, the tap on the delete button will override that of the tap gesture, meaning the delete: method is invoked directly, but on iOS5 (and possibly earlier but I am not targeting those platforms so I didn’t test them), the tap gesture fires and the button’s action does not. Hence the deleteConfirmation: method tests to see if the tap was actually over the delete button or not.

Custom delete confirmationNow I have a nice delete confirmation button that matches all the other buttons in the app, and has been implemented without the knowledge or use of internal classes or views. In addition to the code above, I added a couple of custom methods on my table delegate to dim the other visible controls to avoid any confusion over the delete action, but that is just a little detail in my particular implementation.

I also decided to leave the red minus as is because it doesn’t look as out of place as much as the confirmation button. The only functionality I have not replicated from the default delete confirmation process is animating the red minus back to horizontal if the delete is cancelled. To be able to perform that animation with the standard button would require referencing the internal representation. Alternatively I could replace the button, which I might do if the lack of animation starts to bug me.

Chasing down splotchy bugs

“Is it supposed to look like this?”

Those words were uttered by my wife after I had loaded the latest test version of my app on to a phone so I could demonstrate a new feature. She had wandered off into another area of the app, and as I peaked over her shoulder, I winced in fear as indeed, the screen should not look like that, but nevertheless contained big splotches of transparent pixels where the keyboard was supposed to be. She was trying to type in the name of an item of food but was struggling when half of the keys where randomly invisible.

Corrupt KeyboardThere are moments in software development when you encounter bugs so strange and bizarre that you have no explanation for their cause, that you cannot even conceive of how to achieve the effect if you were trying to on purpose. This was one of the those moments – how on earth do you, at an application level, render chunks of the keyboard to disappear, and different chunks became invisible when pressing different keys, and sometimes those chunks flashed at you, tantalizing and teasing letters?

Of course everything works perfectly on the simulator, just not on the device, and to add insult to the misery, the splotches were ghosts, as grabbing a screenshot failed to include the corruption. I had to resort to using a second camera to capture their existence.

I am on the verge of completing a new app, to be named MealSchedule, that allows you to plan out the dishes you want to eat at future meals. I envisage the app as a reminder of what I’ll be cooking that day and to guide my shopping — a future version will contain some ingredient/shopping list component. I had started the app as a proof of concept and to try out a few UI features, but I had grown to like the direction it was heading in, so I was now focused on launching to the app store. Except there were transparent areas on the screen where they shouldn’t be, and I had no explanation of why.

The main fear, of course, is that this was an internal bug, a problem in iOS that only Apple could fix, a bug which would have an indeterminate timeline and therefore your whole app is scuppered before it can even launch. I’m sure my wife could sense my nervousness as she handed the phone back to me and left me alone to stew.

So first steps, try and isolate the problem. This was the only data entry field in the app, it is UITextView (because I needed multiple lines), but it was wrapped up in a third party control. Let’s quickly rip that out and try a plain UITextView… Hmmm, that works sometimes but the holes return when I use a large font size than the default. That’s odd, feels fishy, and doesn’t make any sense. I try several other things and continue to get inconsistent results. I can’t spot any discernable pattern.

Corrupt KeyboardThe UITextView is on a child view controller that is presented from the main screen. Let’s try dropping an instance on to the main screen (ignore the aesthetics for now). Great, that works perfectly, even the HPGrowingText version. Here was a clue, but it was late, I was tired and I’d had a couple of glasses of wine, to spot it amongst the raft of commented out code and quick hacks.

The next day, refreshed and raring to go because this was a do-or-die bug, I tried a different tack. The app worked perfectly on the simulator, but I had only pushed distinct builds to the device and those earlier versions had not exhibited any corrupt keyboard problems. Therefore I could analyze all the recent changes to determine where the problem was introduced and design a workaround or fix from there, only I wasn’t that sure exactly which build was previously on the phone.

Version control for a one person shop may seem like overkill, but I had been pretty good at committing after each discrete piece of functionality (64 commits in 6 weeks), so I had some pretty good fidelity on changes. Slowly by checking out each previous version and undoing each change, I eventually hit upon the one line — notice how all really strange bugs come down to one line — that caused the corruption or not.

The problematic line?

[_popoverView setClipsToBounds:YES];

A simple instruction telling a view to not draw any content outside of its bounds. Generally something that shouldn’t cause a problem, and while this particular view is an ancestor view of the UITextView, its bounds in this particular instance, were the entire screen and the keyboard was only ever corrupted on the first two rows of letters and not at the bottom. However the one distinguishing feature about this _popoverView was that it also had a shadow. Commenting out the shadow and leaving the clipping in, and the corruption did not appear. The shadow was important, and _popoverView had a subview that I could easily apply the clipping to, all the angst and fear could be addressed in a simple one line change.

So the morals of this story are:

  • Use version control and commit often, even if it is a personal project
  • Test on the device as often as possible, if for no other reason that you have some history. I had, potentially, about a week’s worth of changes to go through since I had last installed on the device
  • Don’t clip a view that has a shadow!

In Defense of Walkthroughs

Clear

Clear

Max Rudberg recently published a post decrying the use of walkthroughs — an introductory series of panels — in an app. He cites Clear, Rise and Solar as “novelty apps” that sacrifice standard interactions for a minimal UI, and as a result have to pay a price of an multi-step tutorial to teach the user how to use the app.

I disagree with Max’s basic premise, I think that walkthroughs/introductions/demos have an important role to play. Clear, Rise, Solar and others should be commended for pushing UI design forward and experimenting with gestures. We are still very early in the era of touch based interfaces and so investigation should be encouraged. But because these gestures are new then there will there has to an some element of education somewhere along the line.

We forget, but touch based smartphones were new at one point in time, and the “basic” gestures were unfamiliar to every-one. But the genius of the Apple ads is they show the iPhone in use with hands and fingers interacting with the device (compare that to the vast majority of the Android ads that stress specifications). The ads were, and still are, an effective walkthrough and tutorial for every app. Without those ads every-one would seriously have to contemplate showing a walkthrough.

Rise Walkthrough

Rise

One of the challenges is selling software, and apps in particular, these days is that there is no real sales process. We browse on-line stores and in a single tap have purchased a product we’ve barely spent any time researching and understanding, and we definitely have not had a guided tour of the app’s compelling features. I suspect very few people read an app’s description — I know that I rarely do, especially if the description is loaded with “voted best …” quotes. A couple of pretty pictures and we’re sold, what’s a buck or two if it doesn’t work out? So as a software developer, our only chance of inserting ourselves into the sales process is in that very first launch. It is only then that we can thank the user for selecting the app and describe a few of the compelling features they might otherwise miss.

Consider a gym club. Once you’ve filled in the paperwork, some-one will show you around the facility pointing out particular items and explaining protocols. Most clubs have “standard” capabilities and I’m sure we could all muddle our way through eventually, but the club doesn’t want to take that chance. They want to ensure that you have the best possible experience and that you will come back. I recognize your app purchase may be on impulse, that there was perhaps very little research, therefore I’m going to take a small amount of your time to set you up for success.

Solar Walkthrough

Solar

I’ve never understand this macho “I don’t need no stinking documentation/training/tutorial” approach, and then get annoyed when it doesn’t do what I want it to do. Many years ago at a previous company, we had a customer who was adamant about not paying for any training or receiving any kind of assistance. Needless to say that he jumped straight in, completely missing a critical component of the setup, and proceeded to bad mouth us for crappy software. That product did not have a walkthrough or something to guide the user to the critical components first, nor did it have a non-standard UI, so we had a user flounder around for a bit, eventually dumping us for a competitor.

Many users are timid, they won’t explore or touch items they don’t undertand, many times they don’t even see an element on the screen even if it conforms to every standard practice in the interaction guidelines. The purpose of a walkthrough should be to encourage the user, to give them the sense of what is possible, to introduce unique features, to say that I care about their success. If that extra minute is what I have to pay to keep a successful customer then I’ll take it.

iOS6 rotation addendum

I wrote previously about handling rotation in iOS6, notably with respect to modal dialogs which I determined are best handled as child view controllers. I recently extended the app that brought to light a situation I wasn’t handling and therefore I need to add an addendum to that post.

The new feature I added was a full screen modal that I did not want to be rotated on the iPhone. The layout was primarily data entry and hence a keyboard would over a significant portion of the screen, so the flow really dictated a portrait only display. I had to tweak the code to prohibit the rotation on this particular view controller.

A very quick reading of the documentation would suggest that adding the following code to the modal view controller would do the job.

- (BOOL)shouldAutorotate {
return IPAD ? YES : NO;
}
- (NSUInteger)supportedInterfaceOrientations {
return IPAD ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskPortrait;
}
view raw gistfile1.m hosted with ❤ by GitHub

We are saying that this view only supports portrait on the iPhone and that we should not auto-rotate. However this does not work as expected, because under iOS6 container view controllers (such as UINavigationController) do not consult their children about rotation events. The root view controller application in my app is a UINavigationController and so the determination as to rotate or not does not go deeper into the view hierarchy and therefore the autoRotate and supportInterfaceOrientations methods on my modal view controller are never called. I am using a base UINavigationController so the rotation methods fall back to their defaults, which on the iPhone is to allow everything except upside down portrait.

The first step is to force the UINavigationController consult its top level view controller about rotation, which is easily managed by a category:

@implementation UINavigationController (Rotation)
// iOS 6
- (BOOL)shouldAutorotate {
return [[self topViewController] shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations {
return [[self topViewController] supportedInterfaceOrientations];
}
@end
view raw gistfile1.m hosted with ❤ by GitHub

My top level view controller is also a container that presents modals as child view controllers, so it needs to consult its children too.

- (BOOL)shouldAutorotate {
BOOL autoRotate = YES;
for (UIViewController *vc in [self childViewControllers]) {
autoRotate = autoRotate && [vc shouldAutorotate];
}
return autoRotate;
}
- (NSUInteger)supportedInterfaceOrientations {
NSUInteger supported = IPAD ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown;
for (UIViewController *vc in [self childViewControllers]) {
supported &= [vc supportedInterfaceOrientations];
}
return supported;
}
view raw gistfile1.m hosted with ❤ by GitHub

So finally this code will ask the modal view controller about rotation and will receive an appropriate answer. This new modal will prohibit rotation, but other simpler and smaller modals still can be rotated.

Google Doesn’t Understand Customer Service

Were the Mayans right? I hadn’t received any new email in Gmail since 2:09 on the afternoon of 12/12/12. Was this the beginning of the end?

I normally have a steady drip of messages into my personal inbox throughout the day, even more so in this holiday period as companies that I have a legitimate relationship with, advise me of their latest offers and free shipping deals. But there it was, on the evening of the 12th — absolute silence. The fact that I as expecting some important messages increased my tension.

Only after some manual refreshing did Gmail inform me that it was having problems retrieving my email, prompting me to dig deeper and eventually eliciting the rather terse message:

SSL Security Error.
Server returned error “SSL error: self signed certificate”

My personal email is hosted by a server that sits in the UK and is managed by my brother. I just use Gmail as a convenient client so that I am not tied to a particular desktop, the spam filtering is first rate without any manual training, and the messages are stored safely on a server. The error message would imply that our server was configured incorrectly and, given that early afternoon in Portland is late evening in the UK, a time when a brother would be making changes to a server, would imply that something had been messed up. I know he doesn’t use Gmail as a client, so he may not be aware of the problems he has caused. But could I raise his attention with texts? No I could not!

Well, Kevan, I hereby apologize for besmirching your good name in my thoughts. You were entirely blameless in this escapade. Through some digging, it appears that Google decided to change their procedures and enforce a strict SSL policy. They would now only connect to a server if it has a valid, signed SSL certificate. Any mail server using a self-signed certificate, which are a common occurrence amongst personally managed mail servers such as ours, would be refused.

As an aside, the error message “Server returned error”, is poorly written because it is not clear as to whose server we are talking about. It is not an error that our mail server is returning a self-signed certificate — that is a legitimate thing to do. The problem is that Google is not allowing such an activity. This is not an SSL error, it is a policy of not accepting certain kinds of configurations. The error message is just lazy engineer speak that fails to convey the correct issue.

Now I don’t disagree with the policy change as it helps to protect from man-in-the-middle attacks, but I do condemn the implementation of the change, and it demonstrates that Google is an engineering company and doesn’t understand customer service.

In any production system, if you are going to introduce a change that a) will disrupt the service, or b) force the customer to perform an action, or c) cause the customer to pay some money, then you need to proactively communicate that change. Google, with this SSL policy enforcement, hit that trifecta and absolutely should have told everyone of the change.

The solution to the problem is for us to purchase an SSL certificate from a reputable authority. No big deal, except that this takes time because our identity has to be verified, except that it costs money, and we have no access to email during the transition. I had to hurriedly configure a desktop email client that I could authorize to overlook a self-signed certificate, but this will be a temporary crutch until we can install a signed certificate, and is something that I shouldn’t have to do. From Google’s perspective that is tempting trouble because I might like the new system and give up on Gmail altogether. Clearly the policy change was not fully thought through.

It would have been trivial for Google to determine all the accounts that fetched email from a remote server and verified which of those servers had a self-signed certificate. It should have then sent those accounts an email with the details, reasons and implications of the upcoming change, along with a timeline for its implementation. In this particular case, because it requires the purchase of an SSL certificate, there should have been at least a week’s notice. You cannot just pull the plug on a service if the solution requires a significant time to implement. It shows a total lack of respect for your customers and their needs.

App Tour : Part 4: Mimicking instance variables for a category

This is the final part of a four part series. The first part discussed dynamically injecting video into a UIWebView and followed on in the second part with how to detect scrolling to the end of the HTML page. The third part covered how to handle full screen video on an iPad.

The previous post in this series discussed how to handle full screen video when that video is launched from a popover on the iPad. A popover is not a standard part of the view hierarchy, so when the video is made full screen the popover remains on top of the video, which of course is not a desired effect. The solution is to recognize when the video enters full screen and hide the popover temporarily and restore it when the video exits.

In my application there are currently two different view controllers that present a popover with the potential to show video. Therefore I wanted to wrap the hide and show effects into a nice neat code unit that I could reuse whenever necessary. In Objective-C that usually means a category. I could encapsulate the new hidePopover: and showPopover: methods in a UIViewController category and import that wherever it was required.

Hiding any old popover is easy, but what I wanted to do was to restore the popover to the exact same state as when it was hidden. As my popover contained a UINavigationController and several UITableViewControllers, I needed to return to a view stack several layers deep. The easiest way to do this is to retain a reference to the popover’s content before dismissing it and using that reference when the popover is recreated.

// Temporarily hide a popover
- (void)hidePopover:(NSNotification *)notification {
self.hiddenPopoverContent = nil;
if (self.hiddenPopoverController) {
// Save some details of the current state
// Holding on to the content view means we can go back to the same state
self.hiddenPopoverContent = [self.hiddenPopoverController contentViewController];
self.hiddenPopoverContentSize = [self.hiddenPopoverController popoverContentSize];
id <UIPopoverControllerDelegate> popoverDelegate = [self.hiddenPopoverController delegate];
self.hiddenPopoverDelegate = popoverDelegate;
[self.hiddenPopoverController dismissPopoverAnimated:YES];
self.hiddenPopoverHidingInProgress = YES;
if (popoverDelegate) {
[popoverDelegate popoverControllerDidDismissPopover:self.hiddenPopoverController];
}
self.hiddenPopoverHidingInProgress = NO;
self.hiddenPopoverController = nil;
}
}
// Show a popover that had previously been hidden
- (void)showPopover:(NSNotification *)notification {
if (self.hiddenPopoverContent) {
UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:self.hiddenPopoverContent];
if ([popover respondsToSelector:@selector(popoverBackgroundViewClass)]) {
[popover setPopoverBackgroundViewClass:[ZingPopoverBackgroundView class]];
}
CGSize popSize = self.hiddenPopoverContentSize;
[popover setPopoverContentSize:popSize animated:YES];
[popover setDelegate:self.hiddenPopoverDelegate];
if (self.hiddenPopoverPresentingButton) {
[popover presentPopoverFromBarButtonItem:self.hiddenPopoverPresentingButton permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
} else {
[popover presentPopoverFromRect:self.hiddenPopoverPresentingRect inView:[self view] permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
}
self.hiddenPopoverController = popover;
}
}
view raw gistfile1.m hosted with ❤ by GitHub

The hidePopover: grabs the popover’s contentViewController before dismissal so that the content is not released, and when showPopover: recreates the popover, it reuses this content. Because the category retains the contentViewController, it’s state is left untouched and the user is returned to the same place in the navigation hierarchy.

Note that hidePopover: also retains information about the size and placement of the popover. Once we have this information then the category does not have to go back to the hosting view controller and ask it to re-present the popover again. As far as the hosting view controller is concerned. nothing has happened.

But the fly in the ointment here, is that categories cannot define new ivars, only methods. So how do we store new information just for the category, without having to extend the main view controller? If we have to modify the base class then that destroys the drop-in effect of the category.

Well, this is were we can drop down and exploit the Objective-C run-time.

With iOS 4.0, Apple introduced associated objects to the Objective-C run-time, providing a mechanism to link two objects together in an ad-hoc manner, without the need to explicitly declare variables. Effectively an object maintains a dictionary to a set of arbitrary objects.

The two main associated object functions are:

#import 

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
id objc_getAssociatedObject(id object, void *key)

The association policy is similar to the standard semantic – OBJC_ASSOCIATION_ASSIGN, OBJC_ASSOCIATION_RETAIN_NONATOMIC, OBJC_ASSOCIATION_COPY_NONATOMIC, OBJC_ASSOCIATION_RETAIN, OBJC_ASSOCIATION_COPY. Therefore an object can retain a reference to another object.

So let’s see how we can wrap this into a neat package for our category.

First we’ll declare a set of private properties and keys for our associations:

static char const * const HiddenPopoverControllerKey = "HiddenPopoverController";
static char const * const HiddenPopoverContentKey = "HiddenPopoverContent";
static char const * const HiddenPopoverDelegateKey = "HiddenPopoverDelegate";
static char const * const HiddenPopoverPresentingRectKey = "HiddenPopoverPresentingRect";
static char const * const HiddenPopoverPresentingButtonKey = "HiddenPopoverPresentingButton";
static char const * const HiddenPopoverArrowDirectionKey = "HiddenPopoverArrowDirection";
static char const * const HiddenPopoverAnimatedKey = "HiddenPopoverAnimated";
static char const * const HiddenPopoverContentSizeKey = "HiddenPopoverContentSize";
static char const * const HiddenPopoverHidingInProgessKey = "HiddenPopoverHidingInProgress";
@interface UIViewController (RBPopoverPrivate)
@property (nonatomic) UIPopoverController *hiddenPopoverController;
@property (nonatomic) UIViewController *hiddenPopoverContent;
@property (nonatomic) id <UIPopoverControllerDelegate> hiddenPopoverDelegate;
@property (nonatomic) CGRect hiddenPopoverPresentingRect;
@property (nonatomic) UIBarButtonItem *hiddenPopoverPresentingButton;
@property (nonatomic) UIPopoverArrowDirection hiddenPopoverArrowDirection;
@property (nonatomic) BOOL hiddenPopoverAnimated;
@property (nonatomic) CGSize hiddenPopoverContentSize;
@property (nonatomic) BOOL hiddenPopoverHidingInProgress;
@end
view raw gistfile1.m hosted with ❤ by GitHub

The properties allow an easy interface to the data in the other methods in the category, e.g. self.hiddenPopoverController and self.hiddenPopoverContent.

In the implementation, we provide our own accessor functions that fetch and store the data as associated objects, with a retain policy, to the base object.

@implementation UIViewController (RBPopoverPrivate)
- (UIPopoverController *)hiddenPopoverController {
return objc_getAssociatedObject(self, HiddenPopoverControllerKey);
}
- (void)setHiddenPopoverController:(UIPopoverController *)hiddenPopoverController {
objc_setAssociatedObject(self, HiddenPopoverControllerKey, hiddenPopoverController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIViewController *)hiddenPopoverContent {
return objc_getAssociatedObject(self, HiddenPopoverContentKey);
}
- (void)setHiddenPopoverContent:(UIViewController *)hiddenPopoverContent {
objc_setAssociatedObject(self, HiddenPopoverContentKey, hiddenPopoverContent, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// plus many more ...
@end
view raw gistfile1.m hosted with ❤ by GitHub

Note, assigning nil to an associated object will release it and remove the association.

Now we have the basics for our category to handle the presentation of a popover when there is a chance that we might need to temporarily hide it due to full screen video.

Here is the full code for the category.

@implementation UIViewController (RBPopover)
- (void)startTrackingHidePopover {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(hidePopover:)
name:NOTIFICATION_POPOVER_HIDE
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showPopover:)
name:NOTIFICATION_POPOVER_SHOW
object:nil];
}
- (void)stopTrackingHidePopover {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NOTIFICATION_POPOVER_HIDE
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NOTIFICATION_POPOVER_SHOW
object:nil];
}
- (void)presentPopover:(UIPopoverController *)popover fromRect:(CGRect)rect permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections animated:(BOOL)animated {
self.hiddenPopoverController = popover;
self.hiddenPopoverPresentingRect = rect;
self.hiddenPopoverPresentingButton = nil;
self.hiddenPopoverArrowDirection = arrowDirections;
self.hiddenPopoverAnimated = animated;
if ([popover delegate]) {
self.hiddenPopoverDelegate = [popover delegate];
}
[popover presentPopoverFromRect:rect inView:[self view] permittedArrowDirections:arrowDirections animated:animated];
}
- (void)presentPopover:(UIPopoverController *)popover fromBarButtonItem:(UIBarButtonItem *)button permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections animated:(BOOL)animated {
self.hiddenPopoverController = popover;
self.hiddenPopoverPresentingButton = button;
self.hiddenPopoverArrowDirection = arrowDirections;
self.hiddenPopoverAnimated = animated;
[popover presentPopoverFromBarButtonItem:button permittedArrowDirections:arrowDirections animated:animated];
}
- (void)dismissedPopover {
// Don't destroy any data if we are in the middle of the hiding process
if (self.hiddenPopoverHidingInProgress) {
return;
}
self.hiddenPopoverDelegate = nil;
self.hiddenPopoverContent = nil;
self.hiddenPopoverController = nil;
self.hiddenPopoverPresentingButton = nil;
}
#pragma mark - Popover notifications
// Temporarily hide a popover
- (void)hidePopover:(NSNotification *)notification {
self.hiddenPopoverContent = nil;
if (self.hiddenPopoverController) {
// Save some details of the current state
// Holding on to the content view means we can go back to the same state
self.hiddenPopoverContent = [self.hiddenPopoverController contentViewController];
self.hiddenPopoverContentSize = [self.hiddenPopoverController popoverContentSize];
id <UIPopoverControllerDelegate> popoverDelegate = [self.hiddenPopoverController delegate];
self.hiddenPopoverDelegate = popoverDelegate;
[self.hiddenPopoverController dismissPopoverAnimated:YES];
self.hiddenPopoverHidingInProgress = YES;
if (popoverDelegate) {
[popoverDelegate popoverControllerDidDismissPopover:self.hiddenPopoverController];
}
self.hiddenPopoverHidingInProgress = NO;
self.hiddenPopoverController = nil;
}
}
// Show a popover that had previously been hidden
- (void)showPopover:(NSNotification *)notification {
if (self.hiddenPopoverContent) {
UIPopoverController *popover = [[UIPopoverController alloc] initWithContentViewController:self.hiddenPopoverContent];
if ([popover respondsToSelector:@selector(popoverBackgroundViewClass)]) {
[popover setPopoverBackgroundViewClass:[ZingPopoverBackgroundView class]];
}
CGSize popSize = self.hiddenPopoverContentSize;
[popover setPopoverContentSize:popSize animated:YES];
[popover setDelegate:self.hiddenPopoverDelegate];
if (self.hiddenPopoverPresentingButton) {
[popover presentPopoverFromBarButtonItem:self.hiddenPopoverPresentingButton permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
} else {
[popover presentPopoverFromRect:self.hiddenPopoverPresentingRect inView:[self view] permittedArrowDirections:self.hiddenPopoverArrowDirection animated:self.hiddenPopoverAnimated];
}
self.hiddenPopoverController = popover;
}
}
@end
view raw gistfile1.m hosted with ❤ by GitHub

To use it, a view controller should call startTrackingHidePopover and use either of the presentPopover: replacements to initially show a popover. When the popover is finally dismissed, call dismissedPopover, and stopTrackingHidePopover when you leave the view.

  • Part 1: Dynamically injecting video into a UIWebView
  • Part 2: Detecting scrolling to the end of a UIWebView page
  • Part 3: Handling full screen video

iOS6 and Rotation Whack-a-Mole

Recently I started a new iOS app primarily to research a few user interaction effects, but it may turn into a real-life app. As a universal app the iPad naturally support all rotations, but on the iPhone I wanted both portrait and landscape views despite the large difference in aspect ratio.

Apple introduced a new way of handling rotation in iOS6, which the documentation covers pretty well. In the long term it should make things simpler, but in the short term developers will need to support the iOS5 way by responding to the shouldAutorotateToInterfaceOrientation: method, and the iOS6 way by coordinating the Info.plist rotation settings, the application delegate’s application:supportedInterfaceOrientationsForWindow: and a view controller’s supportedInterfaceOrientations methods. The easiest way to handle the latter is to create a category for UIViewController:

@implementation UIViewController (Rotation)
// iOS 6
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
return IPAD ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown;
}
@end
view raw gistfile1.m hosted with ❤ by GitHub

These methods indicate which rotations are valid for all views, and indeed work very nicely when you rotate the device. But, and the whole point of this blog post is built around the but, there are a few issues to overcome with iOS6.

Select Meal TypeIn one area of the app, I wanted to present a small modal dialog on top of the current view to allow the user to select an item from a list. A semi-transparent shade layer appears underneath the modal to trap any taps on the screen (taps on this layer cancels the modal much like a UIPopover) but still allow the user to see what is underneath, and the whole presentation is less glaring transition than the standard full screen change, which hides the current context.

The standard iOS way for a view controller to show a modal view is via presentViewController:animated:completion:. It is relatively easy to insert a semi-transparent view into the presented controller’s view hierarchy, add a gesture recognizer to collect the taps on that background and to show a pretty view on top. But, and here comes that but, when you rotate the device the modal view controller rotates very nicely just as it should, the presenting view controller, the one that is visible underneath your shade doesn’t budge. This because iOS on the iPhone presents modals as full screen and saves some work by not sending rotation events to the view controllers earlier in the stack.

This state of affairs caused much gnashing of teeth – the mismatched text orientation between the two levels looked ugly, but I really wanted the semi-transparent effect to reduce the weight of the user interface. I also wanted to retain the modal in its own view controller because I anticipated using the pattern in several places and I didn’t want to load up the main view controller with a bunch of extra views.

I tried many things, some of them fairly ugly hacks, but in the end there was effectively a single line of code solution.

iOS5 introduced the concept of custom container view controllers. There have always been container view controllers such as UINavigationController and UITabBarController in iOS, but now we can implement our own containers with our own logic for transitioning between views or indeed to present several views concurrently on a screen, each one managed by its own controller.

[self addChildViewController:modalViewController];
[[modalViewController view] setFrame:[[self view] bounds]];
[[self view] addSubview:[modalViewController view]];
[modalViewController didMoveToParentViewController:self];
view raw gistfile1.m hosted with ❤ by GitHub

The addChildViewController: call establishes the relationship between the container and modal, then we say whereabouts the child will appear on the screen so that the child’s view is part of the container’s view hierarchy. Custom containers must call didMoveToParentViewController: explicitly to tell the child that the transition is complete. The child container can override this method to react to its state change. When you are ready to delete the child view there is a removeChildViewContainer: method. Containers will automatically forward rotation events to their child controllers if the shouldAutomaticallyForwardRotationMethods returns YES, which happens to be the default.

Bonus tip: setting the modal’s frame before displaying it is important because it avoids any problems with positioning views during rotation. I didn’t do this in my initial implementation and saw some rather bizarre locations of views after rotation.

So there we are, I have the appearance of a modal laid on top of a view, everything visible rotates perfectly, and the main view and modal views have their own controllers for good code separation.