Our DNA is written in Swift
Jump

Getting the Today Widget Look Right

One of my apps has a notification center widget. In this blog post I am exploring a few ingredients which are necessary to have the look go well with Apple’s original widgets.

Ad

Contrary to normal apps, today widgets get “assigned seating”, i.e. iOS places it into the today tab in Notification Center in the place where the user wants it to be.

People view Today widgets in the Today area of Notification Center. Because people configure the Today area so that it displays the information they value most, design your widget with the goal of earning a place among the user’s most important items.

The UI of your widget is contained in a view controller. In my case I derived it from UITableViewController so that I can get the typical table look that other widgets, like the Stocks Widget, examplify.

Today Widget

This screen shot shows several of the important factors for the widget to “feel at home”. The first being the left margin. The HIG instructs us to align the text with the label.

The problem with using a table view is that if you just go with the default margins you get a selection box that does not cover the entire width. To fix this we need to make our widget (and thus the table view) span the entire width, but inset the labels itself.

We learn about the correct inset value from a delegate method. I am storing away the original margin insets for later reference. I return zero for the left margin and only half the proposed value for the bottom. We have a “Last Update” label at the bottom and now separating line at the bottom, so with the original value the space would be way too large.

- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets 
{
   // store default margins for later reference
   _defaultMarginInsets = defaultMarginInsets;
 
   // margins are handled inside of table view
   defaultMarginInsets.left = 0;
 
   // reduce bottom margin because we have an "open" bottom row without separator
   defaultMarginInsets.bottom /= 2.0;
 
   return defaultMarginInsets;
}

iOS apparently automatically configures table view separators to have a vibrancy effect. On the screenshot you can see the horizontal lines changing in color to amplify the blurred springboard behind it. If you were to set the separatorEffect property to nil you get plain lines.

In the view controller’s viewDidLoad I am adding a footerView to the table to hide the bottom-most separator line.

   // automatic row sizing
self.tableView.estimatedRowHeight = 44.0;
 
// get separators to look like Apple's
self.tableView.separatorEffect = [UIVibrancyEffect notificationCenterVibrancyEffect];
self.tableView.separatorColor = [UIColor colorWithWhite:1.0 alpha:0.5];
 
// hide separator on bottom row
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 
                                                               self.tableView.frame.size.width, 1)];
self.tableView.tableFooterView.backgroundColor = [UIColor clearColor];

At first I did have custom table view cells for the main content, but I found that it is simpler to get the left inset if I instead use classic “right detail” table view cells. There you only have to set the separatorInset of the cell to get both separator as well as textLabel to line up with the title.

// setting separatorInset apparently also insets the entire cell
cell.separatorInset = UIEdgeInsetsMake(0, _defaultMarginInsets.left, 0, _defaultMarginInsets.right);

The separators already have the notification center vibrancy effect attached, there are two more things that need it. First the selectedBackgroundView of the cells needs to be a certain shade of vibrant. With some experimenting I found the following to be looking perfect:

UIVibrancyEffect * effect = [UIVibrancyEffect notificationCenterVibrancyEffect];
 
UIVisualEffectView * effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
effectView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
effectView.frame = cell.contentView.bounds;
 
UIView *view = [[UIView alloc] initWithFrame:effectView.bounds];
view.backgroundColor = self.tableView.separatorColor;
view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[effectView.contentView addSubview:view];
cell.selectedBackgroundView = effectView;
 
cell.selectionStyle = UITableViewCellSelectionStyleDefault;

It is still a mystery to me how UIVibrancy uses the contentView’s subview’s colors to arrive at the final effect. But since the section highlight should look exactly like the separators I simply used the seperatorColor for it. You can see the result behind the “Team Volume” row in the screenshot above.

The third vibrant item after separators and selection highlight, is also mentioned by the HIG:

In general, use the system font in white to display text. White text looks good on the default Notification Center background. For secondary text, use the system-provided vibrant appearance.

I already put the main labels in white. The detail labels on the right side depend on where the movement of the number went. This has a bit of a neon touch, primary colors work well there.

Screen Shot 2015-10-25 at 18.33.37

The secondary text in my example is the time the numbers where updated. I had this in solid gray before but in order to match up with the HIG I changed it to be vibrant, too. Also I had it centre-aligned, but since everything is left aligned with the title, I made this one left-aligned as well.

Now how do I get the alignment to work, but still get the vibrancy effect? Turns out it is quite simple if you move the standard textLabel from its usual place in the view hierarchy to be a subview of the effect view’s contentView.

@import NotificationCenter;
 
@interface VisualEffectCell : UITableViewCell
@end
 
@implementation VisualEffectCell
{
   UILabel *_label;
   UIVisualEffectView *_visualEffectView;
}
 
- (void)layoutSubviews
{
   [super layoutSubviews];
 
   if (!_visualEffectView)
   {
      UIVisualEffect *effect = [UIVibrancyEffect notificationCenterVibrancyEffect];        
      _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:effect];
      _visualEffectView.frame = self.contentView.bounds;
      _visualEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
                                           UIViewAutoresizingFlexibleHeight;
      [self.contentView addSubview:_visualEffectView];
 
      [_visualEffectView.contentView addSubview:self.textLabel];
   }
}
 
@end

A final touch you cannot see in the screenshots is that the table view rows are linking to different places in the app. Only the time stamp row is inactive as it only contains supplementary information which is not reflected anywhere in the app.

There is a bug I found when using table view cells to make up your today widget. Due to the magic iOS does to the table view to make it translucent automagically you will find that the table view cells only react to touches if you tap the labels. The workaround is to give cells an invisible background color with a high enough alpha.

// to preserve tappability, without that only labels are active
cell.contentView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.01];

This is ugly and should not be necessary, but it works…

Conclusion

Thanks to Simon Booth who helped me understand the combination of vibrancy and labels. He also inspired me to look up the section on today widgets in the HIG.

If you adhere to the recommendations of the Apple iOS HIG and try to mimic Apple’s style in their own widgets then you can come pretty close to perfect. In summary the elements for perfect Today Widgets are:

  • Left-align all table view rows with the widget title
  • If using table view, make it wide enough for selection to cover whole width
  • Separators, selection background views and secondary text labels should be using the vibrancy effect
  • Link from information to relevant parts of your app

The today screen is the one place in iOS where content from different places is displayed next to each other. Which is why extra value should be placed on getting the look right, as to not insult the user’s sense of beauty.


Categories: Recipes

3 Comments »

  1. The main problem for me with Today Extensions is sharing data between the mother app and the widget, since they have no direct connection. I want to use a shared folder for this (via App Group) to store the mother apps database files in. It would be good to have a closer and more direct connection between Today Extensions and the Mother App in the future, since the Widget is often only a subset of the mother apps functionality and data, and you don’t want to duplicate things.

  2. You are awesome! Congratulations for the post!