Dynamic Sizing

Last time we created a rounded square widget, now lets extend it to allow users to resize it dynamically. There are 3 parts to adding dynamic size which are enabling expand/shrink buttons, handling button taps and updating the content. These buttons are considered as accessories and a few convenience methods are provided to deal with them.

To enable the buttons we need to override isAccessoryTypeEnabled:. This method is called to check if widget should currently enable any of the accessory types it provides (see enum in HSWidgetViewController.h for all the supported accessory types). For simplicity lets enable accessories for following cases:

  • Enable expand if there is enough space to increase by 1 row and 1 column
  • Enable shrink if our current size is more than 1 row and 1 column (i.e. 1 icon space)

So how do we check if there is enough space to increase our widget you might ask? HSWidgetViewController provides containsSpaceToExpandOrShrinkToWidgetSize: convenient method for exactly that reason. This method can be used to check for space availablity of any shape and takes in final size to expand, shrink or change shape to for its argument. For our case there only needs to be a check for expansion by 1 row and 1 column, since there will always be enough space to shrink as we would be giving up space that we already have. For expansion check we need to get current size of widget via self.widgetFrame.size. Putting it all that together:


-(BOOL)isAccessoryTypeEnabled:(AccessoryType)accessoryType {
	if (accessoryType == AccessoryTypeExpand) {
		HSWidgetSize finalExpandedSize = HSWidgetSizeAdd(self.widgetFrame.size, 1, 1);
		return [self containsSpaceToExpandOrShrinkToWidgetSize:finalExpandedSize];
	} else if (accessoryType == AccessoryTypeShrink) {
		return self.widgetFrame.size.numRows > 1 && self.widgetFrame.size.numCols > 1;
	}

	// anything else we don't support but let super class handle it incase new accessory types are added
	return [super isAccessoryTypeEnabled:accessoryType];
}

Compiling and running this code will show the expand accessory.

But thats only enabling the visuals, we still need to handle both expand and shrink events. To do this, accessoryTypeTapped: needs to be overriden. We want to use this method to resize widget and update states of our content based on accessory that was tapped. HSWidgetViewController handles a lot of the widget resizing so we just need to call one of the convenience methods (updateForExpandOrShrinkToWidgetSize:) that handles it.


-(void)accessoryTypeTapped:(AccessoryType)accessoryType {
	if (accessoryType == AccessoryTypeExpand) {
		HSWidgetSize finalExpandSize = HSWidgetSizeAdd(self.widgetFrame.size, 1, 1);
		[self updateForExpandOrShrinkToWidgetSize:finalExpandSize];

		// handle any state changes for expanding to new size
	} else if (accessoryType == AccessoryTypeShrink) {
		HSWidgetSize finalShrinkSize = HSWidgetSizeAdd(self.widgetFrame.size, -1, -1);
		[self updateForExpandOrShrinkToWidgetSize:finalShrinkSize];

		// handle any state changes for shrinking to new size
	}
}

Since we have added dynamic size to our widget, our old appraoch of using 50 for width and height doesn't really make sense anymore. We could go back to using auto layout and not have to worry about any additional sizing, but thats no fun. Instead we could change our content size when the requested size of our container view (self.view) changes. HSWidgets handles these size change notifications by setting the requestedSize property of HSWidgetViewController. This means that we can override the setter to handle our size changes.


-(void)setRequestedSize:(CGSize)size {
	[super setRequestedSize:size];

	// It wouldn't be a square if both the sides weren't equal so find the smallest length from size
	CGFloat length = MIN(size.width, size.height);
	self.square.frame = CGRectMake(5, 5, length - 10, length - 10);
}

Now that we handle both events and update size of our square, we are ready to test it. Deploying it to device, and tapping expand button will animate resizing of our widget even though we didn't add any animations. Thats because HSWidgets sets requestedSize inside an animation block which means our frame changes are happening as part of that animation. That leaves us with the animation ending at:

Oof thats buggy, well thats because we forgot to update our hardcoded implemented of calculatedFrame with the sizing logic that we changed. For practice I'll leave that up to you, the final result after the change should look like this:

Note calculatedFrame only needs to be overridden if custom widget view does not cover the entire size of the widget.