While trying to implement a Today Widget I was having some problems with resizing the widget. To figure out what is going on, I created a bare bones test project.
Here is my understanding of how it is supposed to work:
- indicate that the extension supports the .expanded mode (which causes the system to enable the 'Show More' / 'Show Less' toggle button) by adding the following code to -viewDidLoad:
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;- setup the viewHierarchy using autolayout, so no need to set preferredContentSize on the viewController.
- when the user taps the 'Show more' / /Show Less' button, the following method is called and in this method I can adjust my interface (e.g. remove / add views, change constraints etc):
-(void)widgetActiveDisplayModeDidChange: (NCWidgetDisplayMode)activeDisplayMode withMaximumSize: (CGSize)maxSize- to animate the change in view size, I can implement the following method:
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinatorBased on these assuptions, I set up my widget to have a single view. It has constraints set up to pin top, bottom, leading and trailing to the viewController's view and layoutguides. There is also a height-constraint (set to 250) that is activated in .expanded mode.
The behavior I expect is that the when the user taps 'Show more' for the first time, the widget expands. To achieve this, in -widgetActiveDisplayModeDidChange:withMaximumSize: I activate the height constraint. This should lead to -viewWillTransitionToSize:withTransitionCoordinator: being called where I can have the change animated.
When the user tap 'Show Less' I expect the same sequence: -widgetActiveDisplayModeDidChange:withMaximumSize: is called, I inactivate the height-constraint and in -viewWillTransitionToSize:withTransitionCoordinator: I animate the changes again.
The observed behavior is that the widget does indeed expand the first time the 'Show more' button is tapped. The first time the 'Show Less' button is tapped the widget shrinks back. However, when tapping on 'Show more' again, the widget does not expand, but the button switches to say 'Show Less' nonetheless.
-widgetActiveDisplayModeDidChange:withMaximumSize: is still being called with the correct more, however, activating the constraint gives me some autolayout errors in the console now. -viewWillTransitionToSize:withTransitionCoordinator: is not called. (console output at the end of this post).
Remarkably, when -widgetActiveDisplayModeDidChange:withMaximumSize: is called with a mode of .expanded and I not only activate the height constraint, but set the constant property to maxSize.height, the 'Show more' / 'Show less' button results in the perfect behavior (except for the fact that I don't want my widget to be that large).
You can find the full code below. Note that I do not use a storyboard, but create the view hierarchy in code (done in -viewDidLoad in this example).
This seems very much like a bug to me, but I am hoping that I simply missed some documentation somewhere. Any suggestions?
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>
@interface TodayViewController () <NCWidgetProviding>
@property (nonatomic, strong) NSLayoutConstraint * heigthConstraint;
@end
@implementation TodayViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
UIView * testView = [[UIView alloc] init];
testView.translatesAutoresizingMaskIntoConstraints = NO;
testView.backgroundColor = [UIColor greenColor];
testView.layer.borderColor = [UIColor blueColor].CGColor;
testView.layer.borderWidth = 1;
[self.view addSubview: testView];
NSLayoutConstraint * testViewTop =
[NSLayoutConstraint constraintWithItem: testView
attribute: NSLayoutAttributeTop
relatedBy: NSLayoutRelationEqual
toItem: self.topLayoutGuide
attribute: NSLayoutAttributeBottom
multiplier: 1.0
constant: 4];
NSLayoutConstraint * testViewLeading =
[NSLayoutConstraint constraintWithItem: testView
attribute: NSLayoutAttributeLeading
relatedBy: NSLayoutRelationEqual
toItem: self.view
attribute: NSLayoutAttributeLeading
multiplier: 1.0
constant: 4];
NSLayoutConstraint * testViewTrailing =
[NSLayoutConstraint constraintWithItem: testView
attribute: NSLayoutAttributeTrailing
relatedBy: NSLayoutRelationEqual
toItem: self.view
attribute: NSLayoutAttributeTrailing
multiplier: 1.0
constant: -4];
NSLayoutConstraint * testViewBottom =
[NSLayoutConstraint constraintWithItem: testView
attribute: NSLayoutAttributeBottom
relatedBy: NSLayoutRelationEqual
toItem: self.bottomLayoutGuide
attribute: NSLayoutAttributeTop
multiplier: 1.0
constant: -4];
self.heigthConstraint =
[NSLayoutConstraint constraintWithItem: testView
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 1.0
constant: 400];
[self.view addConstraints: @[testViewBottom, testViewTop,
testViewTrailing, testViewLeading]];
}
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler
{
completionHandler(NCUpdateResultNewData);
}
-(void)widgetActiveDisplayModeDidChange: (NCWidgetDisplayMode)activeDisplayMode withMaximumSize: (CGSize)maxSize
{
switch (activeDisplayMode)
{
case NCWidgetDisplayModeCompact:
self.heigthConstraint.active = NO;
break;
case NCWidgetDisplayModeExpanded:
/
self.heigthConstraint.constant = 250;
self.heigthConstraint.active = YES;
break;
}
}
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context)
{
[self.view layoutIfNeeded];
} completion: NULL];
}
@endConsole output:
TodayTestWidget[26437:1682768] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<_UILayoutSupportConstraint:0x608000286bd0 _UILayoutSpacer:0x6080003a1260'UIVC-topLayoutGuide'.height == 0 (active)>",
"<_UILayoutSupportConstraint:0x608000286ae0 V:|-(0)-[_UILayoutSpacer:0x6080003a1260'UIVC-topLayoutGuide'] (active, names: '|':UIView:0x7fa0fe40b8a0 )>",
"<_UILayoutSupportConstraint:0x608000287990 _UILayoutSpacer:0x6080001bff00'UIVC-bottomLayoutGuide'.height == 0 (active)>",
"<_UILayoutSupportConstraint:0x608000287940 _UILayoutSpacer:0x6080001bff00'UIVC-bottomLayoutGuide'.bottom == UIView:0x7fa0fe40b8a0.bottom (active)>",
"<NSLayoutConstraint:0x608000287a30 UIView:0x7fa0fe40ba40.bottom == _UILayoutSpacer:0x6080001bff00'UIVC-bottomLayoutGuide'.top - 4 (active)>",
"<NSLayoutConstraint:0x608000287710 V:[_UILayoutSpacer:0x6080003a1260'UIVC-topLayoutGuide']-(4)-[UIView:0x7fa0fe40ba40] (active)>",
"<NSLayoutConstraint:0x608000287a80 UIView:0x7fa0fe40ba40.height == 250 (active)>",
"<NSLayoutConstraint:0x608000283980 'UIView-Encapsulated-Layout-Height' UIView:0x7fa0fe40b8a0.height == 110 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x608000287a80 UIView:0x7fa0fe40ba40.height == 250 (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.