WWDC Quick Look 💓 By SwiftGGTeam
Meet the UIKit button system

Meet the UIKit button system

观看原视频

Highlight

iOS 15 引入 UIButtonConfiguration,用声明式配置替代逐属性设置,同时新增 toggle button、pop-up button 和 UIMenu 单选子菜单,让按钮从「能点」进化到「会表达状态」。

核心内容

按钮是每个 App 里最常见的控件,但 UIKit 的按钮 API 多年来一直比较原始。你要设置标题、图标、背景色、圆角、间距,每个属性都要单独调。状态变化时还得手动更新样式,代码分散在各处。

iOS 15 用 UIButtonConfiguration 彻底重构了按钮系统。核心思路是:把按钮的所有视觉属性打包成一个值类型配置,通过修改配置来改变按钮外观。系统提供四种预设样式(plain、gray、tinted、filled),支持标题+副标题+图标组合、动态类型、多行文本,还能显示加载指示器。

更实用的是新增了两种按钮行为:toggle button(像开关一样切换选中状态)和 pop-up button(点击弹出菜单并显示当前选中项)。这两种行为都基于 UIActionUIMenu,不需要额外写代理方法。

用 Configuration 创建按钮

02:13

最简单的入门方式:给一个现有按钮赋值配置。

let signInButton = UIButton(type: .system)
signInButton.configuration = .filled()
signInButton.setTitle("Sign In", for: [])

关键点:

  • UIButton 会自动把用旧 API(如 setTitle)设置的标题和图标整合到 configuration 中
  • 这意味着可以逐步迁移,不需要一次性重写所有按钮代码

定制 Configuration

03:20

用结构体方式配置按钮,所有属性一次设好:

var config = UIButton.Configuration.tinted()
config.title = "Add to Cart"
config.image = UIImage(systemName: "cart.badge.plus")
config.imagePlacement = .trailing
addToCartButton = UIButton(configuration: config, primaryAction: ...)

关键点:

  • imagePlacement 控制图标在文字的哪一侧(.leading.trailing
  • 系统会自动处理图标和文字之间的间距

状态驱动的动态更新

04:45

按钮被按下时图标从线框变实心,这个效果用 configurationUpdateHandler 实现:

addToCartButton.configurationUpdateHandler = { [unowned self] button in
    var config = button.configuration
    config?.image = button.isHighlighted
        ? UIImage(systemName: "cart.fill.badge.plus")
        : UIImage(systemName: "cart.badge.plus")
    config?.subtitle = self.itemQuantityDescription
    button.configuration = config
}

关键点:

  • configurationUpdateHandler 在按钮状态变化时自动调用
  • 闭包内先读取当前配置,修改后再赋值回去
  • isHighlightedisSelectedisEnabled 等状态变化都会触发

外部数据变化时手动触发更新:

private var itemQuantityDescription: String? {
    didSet {
        addToCartButton.setNeedsUpdateConfiguration()
    }
}

关键点:

  • setNeedsUpdateConfiguration() 标记按钮需要更新
  • 系统会在下一个运行循环调用 configurationUpdateHandler

完全自定义样式

08:26

UIBackgroundConfiguration 精细控制背景:

var config = UIButton.Configuration.filled()
config.buttonSize = .large
config.image = UIImage(systemName: "cart.fill")
config.title = "Checkout"
config.background.backgroundColor = .buttonEmporium

let checkoutButton = UIButton(configuration: config, primaryAction: ...)

checkoutButton.configurationUpdateHandler = { [unowned self] button in
    var config = button.configuration
    config?.showsActivityIndicator = self.isCartBusy
    button.configuration = config
}

关键点:

  • buttonSize.small.medium.large 三种预设
  • showsActivityIndicator 为 true 时显示加载动画,必要时替换图标
  • background.backgroundColor 设置自定义背景色,系统仍会自动处理按下和禁用状态的变暗效果

Toggle Button

11:56

Toggle button 每次点击自动切换 isSelected 状态,行为类似 UISwitch 但更紧凑:

let stockToggleAction = UIAction(title: "In Stock Only") { _ in
    toggleStock()
}

let button = UIButton(primaryAction: stockToggleAction)
button.changesSelectionAsPrimaryAction = true
button.isSelected = showingOnlyInStock()

关键点:

  • changesSelectionAsPrimaryAction = true 开启 toggle 行为
  • 点击时系统自动翻转 isSelected
  • 可以在 configurationUpdateHandler 中根据 isSelected 改变外观
  • UIBarButtonItem 也新增了 isSelected 属性,支持同样的 toggle 行为

Pop-up Button

14:30

Pop-up button 点击弹出菜单,选中某项后按钮标题自动更新为选中项:

let colorClosure = { (action: UIAction) in
    updateColor(action.title)
}

let button = UIButton(primaryAction: nil)
button.menu = UIMenu(children: [
    UIAction(title: "Bondi Blue", handler: colorClosure),
    UIAction(title: "Flower Power", state: .on, handler: colorClosure)
])
button.showsMenuAsPrimaryAction = true
button.changesSelectionAsPrimaryAction = true

// 读取当前选中项
updateColor(button.menu?.selectedElements.first?.title)

// 程序化设置选中项
(button.menu?.children[selectedColorIndex()] as? UIAction)?.state = .on

关键点:

  • showsMenuAsPrimaryAction = true 让点击直接弹出菜单
  • changesSelectionAsPrimaryAction = true 让按钮变成 pop-up 行为
  • 按钮标题和图标会自动显示当前选中项
  • selectedElements 始终返回一个元素(单选约束)

单选子菜单

18:18

在 pull-down 菜单里嵌套单选子菜单,比如排序选项:

let sortMenu = UIMenu(title: "Sort By", options: .singleSelection, children: [
    UIAction(title: "Title", handler: sortClosure),
    UIAction(title: "Date", handler: sortClosure),
    UIAction(title: "Size", handler: sortClosure)
])

let topMenu = UIMenu(children: [
    UIAction(title: "Refresh", handler: refreshClosure),
    UIAction(title: "Account", handler: accountClosure),
    sortMenu
])

let sortSelectionButton = UIBarButtonItem(primaryAction: nil, menu: topMenu)

关键点:

  • options: .singleSelection 让子菜单变成单选模式
  • 选中行为自动管理,不需要手动维护 state
  • 嵌套子菜单也适用同样的单选约束

详细内容

四种预设样式

iOS 15 提供四种基础按钮样式:

样式外观适用场景
plain无背景,文字+图标工具栏、导航栏
gray灰色圆角背景次要操作
tinted带主题色的透明背景强调但不过于突出
filled实心填充主题色主要操作

布局控制

UIButton.Configuration 提供精细的布局控制:

var config = UIButton.Configuration.filled()
config.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
config.imagePadding = 8
config.titlePadding = 4
config.titleAlignment = .leading
config.imagePlacement = .top

关键点:

  • contentInsets 控制按钮内容区与边界的间距
  • imagePadding 控制图标与文字之间的间距
  • titlePadding 控制多行标题之间的间距
  • imagePlacement 支持 .leading.trailing.top.bottom

Mac Catalyst 自动适配

15:43

新按钮类型在 Mac Catalyst 上会自动转换为 Mac 原生样式:

  • Pull-down button 变成 Mac 的 bezeled pull-down 按钮
  • Pop-up button 变成 Mac 的 pop-up 按钮
  • Toggle button 变成 Mac 的 toggle 按钮

如果希望保持 iPad 风格(比如突出的自定义按钮),可以设置:

checkoutButton.behaviorStyle = .ipad

核心启发

  1. 替换所有自定义按钮子类

    • 做什么:把项目里继承 UIButton 写的自定义按钮类,逐步迁移到 UIButtonConfiguration
    • 为什么值得做:以前为了做「带图标的圆角按钮」或「按下变色的按钮」,很多人写了 CustomButton: UIButton。现在系统原生支持,代码从几十行降到几行
    • 怎么开始:找一个最简单的自定义按钮,用 .filled().tinted() 配置替换,验证外观一致后批量迁移
  2. 用 Toggle Button 替代 UISwitch

    • 做什么:在设置页、筛选条件等场景,把 UISwitch 换成带文字标签的 toggle button
    • 为什么值得做UISwitch 只有开关状态没有文字说明,用户不知道切换的是什么。Toggle button 把标签和控件合二为一,节省空间且语义更清晰
    • 怎么开始changesSelectionAsPrimaryAction = true + 在 configurationUpdateHandler 里根据 isSelected 切换图标和颜色
  3. 用 Pop-up Button 替代 Segmented Control

    • 做什么:当选项超过 3-4 个时,用 pop-up button 替代 UISegmentedControl
    • 为什么值得做:Segmented control 在选项多时会截断文字或溢出屏幕。Pop-up button 不受宽度限制,还能显示层级结构
    • 怎么开始:创建 UIMenu + showsMenuAsPrimaryAction = true + changesSelectionAsPrimaryAction = true
  4. 做带加载状态的提交按钮

    • 做什么:表单提交按钮在请求过程中显示加载动画,请求完成后恢复
    • 为什么值得做showsActivityIndicator 一行代码实现,系统会自动处理图标替换和动画,不需要手动管理 UIActivityIndicatorView
    • 怎么开始:在 configurationUpdateHandler 里根据请求状态设置 config?.showsActivityIndicator = isLoading
  5. 统一 App 内所有按钮风格

    • 做什么:定义一套 App 级别的按钮配置工厂方法,所有按钮通过工厂创建
    • 为什么值得做UIButtonConfiguration 是值类型,可以复用和组合。定义 primaryButtonConfig()secondaryButtonConfig() 等工厂方法,确保全 App 按钮风格一致
    • 怎么开始:写一个 ButtonStyle 枚举,每个 case 返回预配置好的 UIButton.Configuration

关联 Session

评论

GitHub Issues · utterances