现在UITableView的行高已经可以根据首选字体大小动态改变了,但是BNRItemCell中UILabel对象的文字大小还不会发生任何变化。本节将修改BNRItemCell,支持动态字体。首先查看BNRItemCell各个子视图的现有约束:UIImageView对象位于contentView左边,并保持垂直居中;nameLabel始终位于顶部,serialNumberLabel始终位于底部;valueLabel位于contentView右边,也保持垂直居中(见图20-5)。
图20-5 BNRItemCell各个子视图的现有约束
下面根据用户首选字体设置UILabel对象的font属性,读者可以参考之前在BNRDetailViewController或BNRItemsViewController中添加的代码。
打开BNRItemCell.m,添加下列方法:
- (void)updateInterfaceForDynamicTypeSize
{
UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.nameLabel.font = font;
self.serialNumberLabel.font = font;
self.valueLabel.font = font;
}
- (void)awakeFromNib
{
[self updateInterfaceForDynamicTypeSize];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(updateInterfaceForDynamicTypeSize)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
- (void)dealloc
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
}
以上代码只有awakeFromNib与之前有所区别。当NIB文件解档某个对象后,该对象就会收到awakeFromNib消息,可以在awakeFromNib中设置该对象,或者执行其他需要的初始化操作。例如,以上代码在awakeFromNib中设置了UILabel对象的font属性,并将BNRItemCell对象注册为UIContentSizeCategoryDidChangeNotification的观察者。构建并运行应用,现在BNRItemCell可以根据用户首选字体改变界面中的文字大小了。
接下来需要解决BNRItemCell中的一些自动布局问题。
与之前BNRDetailViewController中的问题类似,第19章限定了nameLabel和serialNumberLabel的高度,自动布局系统将无法修改两个UILabel对象的高度。
打开BNRItemCell.xib,删除两个UILabel对象的高度约束,这时Interface Builder会提示视图位置错误。打开Resolve Auto Layout Issues菜单,选择Update All Frames in Homepwner Item Cell。构建并运行应用,现在UILabel对象会根据用户首选字体自动调整高度。
最后还剩下UIImageView对象。下面将修改UIImageView对象的约束,使UIImageView对象可以根据用户首选字体自动调整大小。
为约束添加插座变量
如果需要修改某个视图的大小或位置,无论是修改为固定值还是修改与其他视图之间的相对关系,都应该修改视图的约束,而不是硬编码视图的frame;否则,当界面需要重新布局时,自动布局系统仍然会根据之前的约束修改视图的frame,从而覆盖硬编码的frame。
为了在运行时根据用户首选字体动态修改UIImageView对象的宽度和高度约束,需要为两个约束添加相应的插座变量。约束是NSLayoutConstraint类的对象,添加插座变量的过程与其他视图对象是相同的。
在BNRItemCell.m的类扩展中,添加并关联宽度和高度约束的插座变量,代码如下:
@interface BNRItemCell ()
@property (nonatomic, weak) IBOutlet NSLayoutConstraint
*imageViewHeightConstraint;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint
*imageViewWidthConstraint;
@end
现在可以在代码中动态修改UIImageView对象的两个约束了。在BNRItemCell.m中,修改updateInterfaceForDynamicTypeSize,获取用户首选字体大小,然后修改宽度和高度约束的限定值(设置NSLayoutConstraint对象的constant属性)。
- (void)updateInterfaceForDynamicTypeSize
{
UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.nameLabel.font = font;
self.serialNumberLabel.font = font;
self.valueLabel.font = font;
static NSDictionary *imageSizeDictionary;
if (!imageSizeDictionary) {
imageSizeDictionary = @{
UIContentSizeCategoryExtraSmall : @40,
UIContentSizeCategorySmall : @40,
UIContentSizeCategoryMedium : @40,
UIContentSizeCategoryLarge : @40,
UIContentSizeCategoryExtraLarge : @45,
UIContentSizeCategoryExtraExtraLarge : @55,
UIContentSizeCategoryExtraExtraExtraLarge : @65
};
}
NSString *userSize =
[[UIApplication sharedApplication] preferredContentSizeCategory];
NSNumber *imageSize = imageSizeDictionary[userSize];
self.imageViewHeightConstraint.constant = imageSize.floatValue;
self.imageViewWidthConstraint.constant = imageSize.floatValue;
}
构建并运行应用,修改用户首选字体,这时UIImageView对象会根据用户首选字体自动调整大小。同时,由于之前限定了nameLabel和serialNumberLabel左边与UIImageView对象右边的距离,因此当UIImageView对象的大小发生变化时,nameLabel和serialNumberLabel也会自动调整位置,保持与UIImageView对象的间距。如果之前限定的是两个UILabel对象左边与父视图的距离,那么UIImageView对象就可能遮住两个UILabel对象,或者留下大片空白。
现在BNRItemCell可以很好地支持动态字体了,但是还需要做最后一处修改。
占位符约束
以上代码同时修改了UIImageView对象的宽度和高度约束,虽然这样做没有问题,但是还有一种更好的解决方案:只修改UIImageView对象的高度约束,然后添加另一个约束,限定UIImageView对象的宽度与高度相同。该约束无法在Interface Builder中添加,必须通过代码创建。回到BNRItemCell.m,在awakeFromNib中为UIImageView对象(thumbnailView属性)添加该约束,代码如下:
- (void)awakeFromNib
{
[self updateInterfaceForDynamicTypeSize];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(updateInterfaceForDynamicTypeSize)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
NSLayoutConstraint *constraint =
[NSLayoutConstraint constraintWithItem:self.thumbnailView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.thumbnailView
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0];
[self.thumbnailView addConstraint:constraint];
}
现在可以删除imageViewWidthConstraint属性和相关代码:
@interface BNRItemCell ()
@property (nonatomic, weak) IBOutlet NSLayoutConstraint
*imageViewHeightConstraint;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint
*imageViewWidthConstraint;
@end
@implementation
- (void)updateInterfaceForDynamicTypeSize
{
// 这里省略了其他代码……
NSNumber *imageSize = imageSizeDictionary[userSize];
self.imageViewHeightConstraint.constant = imageSize.floatValue;
self.imageViewWidthConstraint.constant = imageSize.floatValue;
}
打开BNRItemCell.xib,删除UIImageView对象与imageViewWidthConstraint的关联。
最后还有一个问题:UIImageView对象现在有两个宽度约束。除了以上代码创建的之外,XIB文件中也添加过宽度约束。如果在运行时UIImageView对象的宽度发生变化,就会造成约束冲突。
为了解决该问题,需要删除Interface Builder中添加的宽度约束,但是这么做会导致Interface Builder提示视图位置错误或者布局有歧义。因此,更好的方法是将宽度约束设置为占位符约束(placeholder constraint)。自动布局系统会在构建时移除占位符约束,占位符约束并不会真正对视图起作用。
在BNRItemCell.xib中选中UIImageView对象的宽度约束,然后打开属性检视面板,选中标题为Placeholder的选择框。Placeholder选择框后面的描述也解释了“Placeholder”的含义:Remove at build time(在构建时移除),如图20-6所示。构建并运行应用,界面不会有任何变化,但是现在UIImageView对象的宽度约束是根据高度约束定义的。
图20-6 占位符约束