WWDC Quick Look 💓 By SwiftGGTeam
What's new in UIKit

What's new in UIKit

观看原视频

Highlight

iOS 16 为 UIKit 增加了桌面级导航栏、UICalendarViewUIPasteControl、自定义 sheet detent、self-resizing cells 和 UIHostingConfiguration,让 iPad、iPhone 与 Mac Catalyst app 可以用系统 API 完成更多生产力界面。


核心内容

UIKit 在 iOS 16 的主题很明确:把很多以前需要自定义控件、手写布局或平台分支的工作,收进系统 API。

文档类 app 是第一类受益者。导航栏增加 browser 和 editor 两种样式,支持可自定义的中央工具项、自动溢出菜单、标题菜单,以及 Mac Catalyst 中与 NSToolbar 的自动集成(01:22)。

文本编辑也更接近桌面体验。内建 UITextViewWKWebView 只需要打开一个 flag,就可以启用 find and replace;旧的 UIMenuControllerUIEditMenuInteraction 取代,用同一套机制覆盖触摸菜单和指针上下文菜单(03:29)。

控件层面,Apple 把 inline calendar 从 UIDatePicker 中拆成独立的 UICalendarView。它支持单选、多选、禁用单个日期和日期装饰。UIPageControl 也支持自定义当前页与非当前页的指示器图片,并可以配置方向(06:13)。

还有两类变化会影响日常代码。UISheetPresentationController 支持自定义 detent,cell 在内容变化后可以自动重新计算尺寸,UIKit cell 还可以直接用 UIHostingConfiguration 写 SwiftUI 内容(12:0319:0521:25)。

详细内容

用 UICalendarView 做可多选的日历

UICalendarView 使用日期组件 DateComponents 表示日期,避免把日历日期误当成时间点 Date。这是一个重要差别。日历里的“6 月 10 日”需要结合具体 calendar 才有意义。因此,session 特别提醒:如果你的业务需要 Gregorian calendar,要显式设置(07:00)。

// Configuring a calendar view with multi-date selection

let calendarView = UICalendarView()
calendarView.delegate = self
calendarView.calendar = Calendar(identifier: .gregorian)
view.addSubview(calendarView)

let multiDateSelection = UICalendarSelectionMultiDate(delegate: self)
multiDateSelection.selectedDates = myDatabase.selectedDates()
calendarView.selectionBehavior = multiDateSelection

func multiDateSelection(
    _ selection: UICalendarSelectionMultiDate,
    canSelectDate dateComponents: DateComponents
) -> Bool {
    return myDatabase.hasAvailabilities(for: dateComponents)
}

关键点:

  • UICalendarView() 创建独立日历控件,不再把 inline calendar 绑在 UIDatePicker 上。
  • calendarView.delegate = self 让当前对象负责提供日期装饰等回调。
  • calendarView.calendar = Calendar(identifier: .gregorian) 明确使用 Gregorian calendar,避免依赖用户当前 calendar 类型。
  • UICalendarSelectionMultiDate(delegate: self) 创建多日期选择行为。
  • selectedDates 从数据模型恢复已选日期,让日历初始状态与业务数据一致。
  • calendarView.selectionBehavior = multiDateSelection 把选择规则挂到日历控件上。
  • canSelectDate 返回 false 时,该日期会显示为灰色,用户不能选择。

给日历日期加装饰

日历不只负责选择日期。它还可以把某一天的状态显示出来,比如忙碌、出行、聚会。做法是实现 calendarView(_:decorationFor:),根据数据模型返回不同的 UICalendarView.Decoration09:07)。

// Configuring Decorations
func calendarView(
    _ calendarView: UICalendarView,
    decorationFor dateComponents: DateComponents
) -> UICalendarView.Decoration? {
    switch myDatabase.eventType(on: dateComponents) {
    case .none:
        return nil
    case .busy:
        return .default()
    case .travel:
        return .image(airplaneImage, color: .systemOrange)
    case .party:
        return .customView {
            MyPartyEmojiLabel()
        }
    }
}

关键点:

  • decorationFor dateComponents 使用日期组件查询业务状态。
  • return nil 表示这一天没有装饰。
  • .default() 显示系统默认的灰色圆点。
  • .image(airplaneImage, color: .systemOrange) 用图片和颜色表达出行状态。
  • .customView { MyPartyEmojiLabel() } 为特殊状态提供自定义视图。
  • transcript 明确说明,自定义装饰视图不能交互,并且会被裁剪到可用空间内。

自定义 UIPageControl 的方向和指示器

UIPageControl 在 iOS 16 支持两个以前常见的自定义需求:竖向页面指示器,以及当前页和非当前页使用不同图片(09:53)。

// Vertical page control with custom indicators

pageControl.direction = .topToBottom
pageControl.preferredIndicatorImage = UIImage(systemNamed: "square")
pageControl.preferredCurrentIndicatorImage = UIImage(systemNamed: "square.fill")

关键点:

  • direction = .topToBottom 把 page control 改成从上到下排列。
  • preferredIndicatorImage 设置普通页面的指示器图片。
  • preferredCurrentIndicatorImage 设置当前页面的指示器图片。
  • 示例使用 SF Symbols 的 squaresquare.fill,不需要自己画点或维护选中状态。

用自定义 detent 控制 sheet 高度

iOS 15 引入 sheet detent 后,开发者可以在系统的中等和大尺寸之间切换。iOS 16 增加 .custom detent,可以直接返回固定高度,或基于最大高度计算比例(12:03)。

// Create a custom detent
sheet.detents = [
    .large(),
    .custom { _ in
        200.0
    }
]

关键点:

  • sheet.detents 定义 sheet 可以停靠的高度集合。
  • .large() 保留系统的大尺寸状态。
  • .custom { _ in 200.0 } 增加一个 200 point 高的自定义状态。
  • session 提醒,闭包返回值不需要计算 bottom safe area inset,系统会处理 floating 和 edge-attached sheet 的差异。

如果高度需要跟容器空间相关,可以读取 context.maximumDetentValue12:38)。

// Create a custom detent
sheet.detents = [
    .large(),
    .custom { context in
        0.3 * context.maximumDetentValue
    }
]

关键点:

  • context 提供当前环境下的最大 detent 高度。
  • 0.3 * context.maximumDetentValue 表示自定义 detent 使用最大高度的 30%。
  • 这种写法比固定 point 更适合横竖屏切换和不同设备尺寸。

当 detent 需要被其他 API 引用时,先定义 identifier,再把它传给 .custom12:42)。

// Define a custom identifier
extension UISheetPresentationController.Detent.Identifier {
    static let small = UISheetPresentationController.Detent.Identifier("small")
}

// Assign identifier to custom detent
sheet.detents = [
    .large(),
    .custom (identifier: .small) { context in
        0.3 * context.maximumDetentValue
    }
]

// Disable dimming above the custom detent
sheet.largestUndimmedDetentIdentifier = .small

关键点:

  • 扩展 UISheetPresentationController.Detent.Identifier,给自定义 detent 一个稳定名称。
  • .custom(identifier: .small) 创建可被引用的自定义 detent。
  • largestUndimmedDetentIdentifier = .small 让 sheet 停在 small 及以下状态时不遮罩后面的内容。

让 cell 随内容变化自动调整高度

以前,动态高度 cell 的麻烦在更新之后。内容变了,cell 不一定立即重新计算高度。iOS 16 中,UICollectionViewUITableView 的 cell 支持 self-resizing:可见 cell 内部内容变化时,容器可以自动触发尺寸更新(19:05)。

这部分 session 没有给出完整代码片段,但明确给出了 API 入口:

collectionView.selfSizingInvalidation = .enabled

cell.contentView.invalidateIntrinsicContentSize()

关键点:

  • selfSizingInvalidationUICollectionViewUITableView 上的新属性,用来控制 cell 内容变化后的尺寸失效行为。
  • 使用 UIListContentConfiguration 配置 cell 时,configuration 变化会自动触发 invalidation。
  • 其他场景可以在 cell 或 contentView 上调用 invalidateIntrinsicContentSize()
  • 如果使用 Auto Layout,可以选择 enabledIncludingConstraints,让 content view 内部约束变化也触发尺寸更新。
  • 需要无动画更新时,把 invalidation 调用包在 performWithoutAnimation 中。

在 UIKit cell 里直接写 SwiftUI

UIKit 和 SwiftUI 混用,以前常见做法是塞一个 hosting controller 或 hosting view。iOS 16 增加 UIHostingConfiguration,让 collection view 和 table view cell 可以直接用 SwiftUI 描述内容(21:43)。

cell.contentConfiguration = UIHostingConfiguration {
    VStack {
        Image(systemName: "wand.and.stars")
            .font(.title)
        Text("Like magic!")
            .font(.title2).bold()
    }
    .foregroundStyle(Color.purple)
}

关键点:

  • cell.contentConfiguration = UIHostingConfiguration { ... } 把 SwiftUI 内容作为 cell 的 content configuration。
  • VStack 负责垂直排列图标和文字。
  • Image(systemName: "wand.and.stars") 使用 SF Symbol。
  • .font(.title) 设置图标字号。
  • Text("Like magic!") 显示文字。
  • .font(.title2).bold() 设置文字样式。
  • .foregroundStyle(Color.purple) 给整个 SwiftUI 内容设置紫色前景样式。
  • transcript 强调,这种方式不需要额外 view 或 view controller。

核心启发

  • 做什么:给文档编辑 app 加一个桌面级标题菜单。 为什么值得做:iOS 16 的 navigation title menu 可以自动显示 duplicate、move、rename、export、print 等标准项。 怎么开始:使用 browser 或 editor navigation style,并实现对应 delegate 方法;需要额外命令时再添加自定义菜单项。

  • 做什么:做一个可选择多个可预约日期的排期界面。 为什么值得做UICalendarView 支持多选、禁用单个日期和日期装饰。 怎么开始:创建 UICalendarSelectionMultiDate,用 canSelectDate 过滤无库存日期,再用 decorationFor 标记忙碌、出行或特殊活动。

  • 做什么:把底部 sheet 做成 30% 高度的轻量控制面板。 为什么值得做:自定义 detent 可以按最大高度计算,不再局限于系统的 medium 和 large。 怎么开始:用 .custom { context in 0.3 * context.maximumDetentValue } 创建 detent,并在需要时给它定义 identifier。

  • 做什么:让评论、富文本或设置项 cell 在内容变化后自动变高。 为什么值得做:iOS 16 的 self-resizing cells 会合并尺寸失效请求,并在合适时机更新 table view 或 collection view。 怎么开始:检查 selfSizingInvalidation,使用 UIListContentConfiguration 时优先依赖自动 invalidation;自定义内容变化时调用 invalidateIntrinsicContentSize()

  • 做什么:在已有 UIKit 列表中逐步引入 SwiftUI cell。 为什么值得做UIHostingConfiguration 可以直接作为 cell 的 content configuration,不需要额外 hosting controller。 怎么开始:从一个视觉独立的 cell 开始,把 cell.contentConfiguration 替换为 UIHostingConfiguration { ... }

关联 Session

评论

GitHub Issues · utterances