Jun 10

试玩UIButtonConfiguration(iOS15)

Lrdcq , 2021/06/10 13:12 , 程序 , 閱讀(4293) , Via 本站原創
iOS15在UIKit中的改动并不多,其中一大项目是为UIButton新增了UIButtonConfiguration功能,为标准化button组件样式复用增加了更多的可能性。
之前我提过从里氏替换原则的角度,目前我们常用的复用UIButton的方法——继承+约束,其实是不合理的,那UIButtonConfiguration能解决我们的问题么?看起来不能。

基本使用

按文档上的说法,这个工具的使用很简单,即:
UIButtonConfiguration *config = [UIButtonConfiguration grayButtonConfiguration];//从grayButton模版创建
config.baseForegroundColor = [UIColor redColor];//前景色(文字颜色)
config.baseBackgroundColor = [UIColor orangeColor];//背景色
config.cornerStyle = UIButtonConfigurationCornerStyleCapsule;//圆角样式
config.buttonSize = UIButtonConfigurationSizeMedium;//按钮大小

UIButton *btn = [UIButton new];
btn.configuration = config;
[btn setTitle:@"按钮" forState:UIControlStateNormal];
[self.view addSubview:btn];
//再做约束

但是此处看起来就很奇怪,如果我想通过Configuration表示一个按钮完整的样式状态的话,应该比如除了baseForegroundColor,再来一个highlightForegroundColor,或者设置属性的时候区分普通的configuration与highlightConfiguration或者setConfiguration:forState:。

然而UIButtonConfiguration并不能这样用。官方给出的UIButtonConfiguration用法只作用于塞初始态,并没有控制状态扭转后样式的方法,或者说,可以自己去实现(那就是后话了)。

观察UIButtonConfiguration配套的方法,UIButton新增的核心方法包括:
/// Setting a non-nil value for `configuration` will opt into configuration-based behavior on UIButton, update the button in a platform specific manner, and enable/disable some API.
// 设置configuration属性后,UIButton将进入configuration模式,这个模式下生命周期和api和之前有所不同
@property (nonatomic, readwrite, copy, nullable) UIButtonConfiguration *configuration API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

/// Requests the view update its configuration for its current state. This method is called automatically when the button's state may have changed, as well as in other circumstances where an update may be required. Multiple requests may be coalesced into a single update at the appropriate time.
- (void)setNeedsUpdateConfiguration API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

/// Subclasses should override this method and update the button's `configuration`. This method should not be called directly, use `setNeedsUpdateConfiguration` to request an update.
- (void)updateConfiguration API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

/// Block-based equivalent to overriding -updateConfiguration in a subclass. Setting this handler will force the button into configuration-based behavior (see the `configuration` property). This block is called after `-updateConfiguration`
@property (nonatomic, readwrite, copy, nullable) UIButtonConfigurationUpdateHandler configurationUpdateHandler API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

/// When YES, the button will automatically call -updatedConfigurationForButton: on its `configuration ` when the button's state changes, and apply the updated configuration to the button. The default value is YES.
@property (nonatomic, readwrite, assign) BOOL automaticallyUpdatesConfiguration API_AVAILABLE(ios(15.0), tvos(15.0), watchos(8.0));

同时,UIButtonConfiguration的关键方法显然是:
/// Returns a copy of the configuration updated based on the given button, by applying the configuration's default values for that button's state to any properties that have not been customized.
- (instancetype)updatedConfigurationForButton:(UIButton *)button;

运作模式

对以上方法断点,我们能大致理清楚所谓configuration模式下,UIButton的运作模式。

1. configuration模式下,UIButton的刷新主要有3个类:UIButton -> UIButtonConfigurationVisualProvider -> UIButtonConfiguration。大体上的逻辑是UIButton的初始化/attached/statechange触发属性改变的request,通过UIButtonConfigurationVisualProvider传递给UIButtonConfiguration,UIButtonConfiguration结合之前的状态生成最终的Configuration,再去刷新UIButton。

2. 大概是这样子:
點擊在新視窗中瀏覽此圖片

这张图应该解释得比较清楚了,文字简要描述,每次configuration模式下uibutton更新,要做的事情按顺序包括:

a. 根据uibutton的设置,从上一个UIButtonConfiguration生成当前状态的UIButtonConfiguration。这个过程受到automaticallyUpdatesConfiguration控制。
b. 再根据当前的configstate,uibutton按需刷新config。state为新的汇总对象:
<_UIButtonConfigurationState: 0x600001ce9980; traitCollection = <UITraitCollection: 0x600002df0870; UserInterfaceIdiom = Phone, DisplayScale = 2, DisplayGamut = P3, HorizontalSizeClass = Compact, VerticalSizeClass = Regular, UserInterfaceStyle = Light, UserInterfaceLayoutDirection = LTR, ForceTouchCapability = Unavailable, PreferredContentSizeCategory = L, AccessibilityContrast = Normal, UserInterfaceLevel = Base>; Highlighted>

c. 在调用用户感知的生命周期方法updateConfiguration和block版那个。
d. 前面3步button上的configuration应该更新完毕了,最后调用button的apply将config操作到界面上。

3. 以上几个关键流程,以config的updatedConfigurationForButton为节点,调用堆栈如下:

a. 构造器setConfiguration -> UIButtonConfigurationVisualProvider初始化
點擊在新視窗中瀏覽此圖片

b. 构造器setConfiguration -> 第一次塞button数据
點擊在新視窗中瀏覽此圖片

c. add to superview触发布局更新导致
點擊在新視窗中瀏覽此圖片

d. 事件触发state change进而layout触发刷新
點擊在新視窗中瀏覽此圖片

Apple抽象的思路

我们注意到UIButtonConfiguration全程基本上到传递过程中都是clone / copy and change这个模式在使用。观察UIButtonConfiguration的初始化,单一个如上代码的过程UIButtonConfiguration竟然构造了8份。这样设计只能有一个目的:UIButtonConfiguration表示的是UIButton的ui状态

因此我们可以把上面的使用过程描述为:

1. setconfig是为UIButton赋予初始ui状态机。

2. 任何布局/状态的变动,在config覆盖的范围不会直接修改ui,而是会引起状态机的刷新。

3. 状态机刷新后,自动apply状态机到实际ui上。


因此,我们意识到UIButtonConfiguration的复用,本子上是多个按钮之间,ui状态机的镜像复用。而状态机的扭转逻辑,并不能通过UIButtonConfiguration复用。同时,也知道状态机的扭转逻辑,主要是由未暴露的内部类UIButtonConfigurationVisualProvider驱动,并在UIButton和Provider各层中实现。虽然自带UIButton的状态机实际扭转的业务逻辑确实不复杂。

听起来合理的实践方式

结合以上信息,我们确认的是:

1. 我们不能指望UIButtonConfiguration就能解决按钮样式复用问题(当然,如果觉得苹果自带的状态扭转逻辑符合ui诉求,还真可以就这样使用,这也是苹果预期的使用方式,即按钮的hover等效果都是由apple的统一规范解决)。
2. UIButtonConfigurationVisualProvider未暴露,我们不能直接复用Provider的方式解决问题。

那么我们剩下的复用路径可以是:

1. 完全摒弃UIButtonConfigurationVisualProvider中button状态扭转的逻辑(就是automaticallyUpdatesConfiguration属性控制的那段),而是button事件驱动我们一段自己的逻辑去自己创建新状态的Configuration,并且回写。如下:
點擊在新視窗中瀏覽此圖片

2. 因此我们需要一个统一的继承类XXButton,新的基类Provider绑定。新的Provider完全可以继承自UIButtonConfiguration,同时处理状态保存与状态扭转逻辑。因此API看起来会如下:
@interface XXTransferableButtonConfiguration : UIButtonConfiguration

- (instancetype)configurationWithButton:(UIButton *)button action:(UIAction *)action;

@end

@interface XXButton : UIButton

@property (nonatomic, strong, nullable) XXTransferableButtonConfiguration *transferableConfiguration;

@property (nonatomic, readwrite, copy, nullable) UIButtonConfiguration *configuration NS_UNAVAILABLE;

@end
logo