WWDC Quick Look 💓 By SwiftGGTeam
Add custom views and modifiers to the Xcode Library

Add custom views and modifiers to the Xcode Library

观看原视频

Highlight

Xcode 12 通过 LibraryContentProvider 让开发者把自定义 SwiftUI View 和 modifier 注册成可拖拽的 LibraryItem,Xcode 扫描源码后会在 Library 中显示这些条目,并在插入时保持 Preview 继续渲染。


核心内容

SwiftUI 项目变大以后,可复用 View 往往藏在不同文件和 package 里。团队成员知道系统的 ButtonText 可以从 Xcode Library 拖出来,却未必知道项目里已经有一个写好的 SmoothieRowView。如果每次都靠搜索文件名和手写初始化参数,Preview 的可视化编辑优势会被打断。

Xcode 12 把这件事放回 Swift 代码里解决。开发者声明一个符合 LibraryContentProvider 的类型,把重要的 View 或 modifier 包装成 LibraryItem。这些条目会出现在 Xcode Library,用户可以拖进代码或 Canvas,插入的内容仍是可编译的 Swift 示例,Preview 也会继续刷新。

演示项目 Fruta 里,SmoothieRowView 原本只是在多个界面里复用的普通 SwiftUI View。把它注册到 Library 后,Xcode 会自动发现这个条目,为它生成项目分类和标题。插入时使用 .lemonberry 作为占位数据,参数会被 tokenized,开发者可以直接替换成当前列表里的 smoothie

View 不是唯一入口。演讲后半段把 Image 上常见的 resizableaspectRatioframe 三连调用抽成 resizedToFill(width:height:),再把它放进 modifiers library。Xcode 用 base 参数分清被修饰的对象和真正要插入的 modifier,最后只把 modifier 调用写回代码。

这个机制不要求运行项目。Xcode 会扫描 workspace 里的源码,寻找 LibraryContentProvider 声明;这些代码不会在发布版本执行,也会在构建分发产物时被剥离。因为扫描范围包含依赖项,Swift package 也可以把自己的 View 暴露到 Library 中,Fruta 示例里的 Nutrition Facts package 就通过这种方式提供了 CalorieCountView


详细内容

LibraryContentProvider 是入口(01:57

要扩展 Xcode Library,先声明一个符合 LibraryContentProvider 的类型。协议有两个入口:views 提供 View 条目,modifiers(base:) 提供 modifier 条目。两者都返回 LibraryItem 数组。

public protocol LibraryContentProvider {
  @LibraryContentBuilder
  var views: [LibraryItem] { get }

  @LibraryContentBuilder
  public func modifiers(base: ModifierBase) -> [LibraryItem]
}

关键点:

  • views 用来扩展 Xcode 的 views library。
  • modifiers(base:) 用来扩展 modifiers library。
  • @LibraryContentBuilder 允许一个 provider 返回多个 LibraryItem
  • Provider 放在 Swift 代码旁边,编译器能在 API 变化时帮助发现条目是否还能工作。

一个条目就是一段可插入的完成内容(02:32

LibraryItem 最少需要一段会被插入的 completion。它还可以带 visibility、title、category。演示里,SmoothieRowView 使用 .lemonberry 作为默认数据,category 选择 .control

LibraryItem(
  SmoothieRowView(smoothie: .lemonberry),
  visible: true,
  title: "Smoothie Row View",
  category: .control
)

关键点:

  • 第一行的 SmoothieRowView(smoothie: .lemonberry) 是用户选择条目后插入到代码里的内容。
  • visible 控制条目是否展示在 Library 中。
  • title 避免 Xcode 只根据 completion 推断标题。
  • category: .control 让条目进入更明确的功能分类,演示中图标也随之变成 control 风格。

同一个 View 可以有多个配置(03:22

演讲里强调 View 和 LibraryItem 不需要一一对应。SmoothieRowView 可以有普通版本,也可以有打开 showNearbyPopularity 的版本。第二个条目用自定义 title 说明差异。

struct LibraryContent: LibraryContentProvider {
    @LibraryContentBuilder
    var views: [LibraryItem] {
        LibraryItem(
            SmoothieRowView(smoothie: .lemonberry),
            category: .control
        )

        LibraryItem(
            SmoothieRowView(smoothie: .lemonberry, showNearbyPopularity: true),
            title: "Smoothie Row View With Popularity",
            category: .control
        )
    }
}

关键点:

  • LibraryContent 这个类型名没有特殊含义,演示只是用它承载 provider。
  • 第一个 LibraryItem 插入普通 SmoothieRowView
  • 第二个 LibraryItem 插入带 showNearbyPopularity: true 的配置。
  • title 让用户在 Library 搜索结果里区分两个相似条目。

modifier 先从重复调用中抽出来(08:57

Fruta 项目里经常把 resizableaspectRatioframe 连续用在图片上。演讲先把这三步封装成 Image extension,再把这个能力加入 modifiers library。

extension Image {
    func resizedToFill(width: CGFloat, height: CGFloat) -> some View {
        return self
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: width, height: height)
    }
}

关键点:

  • resizedToFill(width:height:) 声明在 Image 上,所以后面的 provider 要以 Image 作为 base。
  • resizable() 让图片可以改变尺寸。
  • aspectRatio(contentMode: .fill) 保持宽高比例并填满目标区域。
  • frame(width:height:) 给最终视图设置宽高。

base 告诉 Xcode 哪部分会被保留(09:17

注册 modifier 时,modifiers(base:) 的参数类型要匹配 modifier 所在的类型。这里是 Image。Xcode 通过 base 区分被修饰对象和要插入的 modifier,用户选择条目后只会得到 .resizedToFill(width:height:) 这段调用。

@LibraryContentBuilder
    func modifiers(base: Image) -> [LibraryItem] {
        LibraryItem(
            base.resizedToFill(width: 100.0, height: 100.0)
        )
    }

关键点:

  • base: Image 对应 Image extension 上的 resizedToFill
  • LibraryItem 的 completion 必须包含 base,这样 Xcode 才知道 modifier 附着在哪里。
  • 100.0 是示例参数,插入后参数会 tokenized,开发者可以按上下文替换。
  • 这个条目出现在 modifiers tab,而不是 views tab。

Xcode 扫描源码生成 Library 内容(10:41

演讲最后说明,Xcode 不需要 build 或 run 项目就能填充 Library。它会扫描源码里的 LibraryContentProvider 并解析声明,所以 UI 重构到一半、项目暂时跑不起来时,Library 内容仍然可以贡献出来。

这套扫描也覆盖 workspace 里的依赖。Fruta 依赖的 Nutrition Facts Swift package 暴露了 CalorieCountView,Xcode 会为 package 创建分类。开发者不打开 package 源码,也能在 Library 中找到并插入这个 View。


核心启发

  1. 设计系统组件面板 做什么:把项目里的按钮、卡片、列表行、空状态 View 注册到 Xcode Library。 为什么值得做:演讲里的核心痛点是 API 可发现性,Library 是开发者寻找可视内容的入口。 怎么开始:为组件所在模块添加 LibraryContentProvider,在 views 里用真实 sample data 返回多个 LibraryItem

  2. 为同一个组件提供常用状态 做什么:给一个 View 准备 normal、loading、empty、error 或带附加信息的多个 Library 条目。 为什么值得做:SmoothieRowView 在演示中用两个 LibraryItem 表示普通版本和 popularity 版本。 怎么开始:在同一个 views builder 中多次创建 LibraryItem,用 title 写清每个配置的用途。

  3. 把高频 modifier 链做成可插入条目 做什么:把图片裁剪、圆角、阴影、固定尺寸等重复 modifier 链封装后放进 modifiers library。 为什么值得做:演讲把 resizableaspectRatioframe 合成 resizedToFill(width:height:),减少重复手写。 怎么开始:先在目标类型上写 extension,再实现 modifiers(base:),用 base.yourModifier(...) 返回 LibraryItem

  4. 给 Swift package 附带可视示例 做什么:在共享 package 里为公开 View 提供 Library 内容,让使用方从 Xcode Library 直接发现它。 为什么值得做:Xcode 会扫描 workspace 依赖,演示中的 Nutrition Facts package 因此出现在 Library 里。 怎么开始:把 provider 放在 package 源码中,给每个公开 View 准备可编译的 sample data 和清晰标题。


关联 Session

  • Structure your app for SwiftUI previews — 讲如何用 sample data、数据流和输入设计,让 SwiftUI views 更容易预览和测试。
  • Visually edit SwiftUI views — 展示用 Xcode Previews canvas 迭代 SwiftUI 界面,并在不同环境下检查 UI。
  • Introduction to SwiftUI — 从 declarative code、SwiftUI view 组合和 Xcode 协作开始,为自定义 View 的 Library 条目提供基础。
  • What’s new in SwiftUI — 概览 2020 年 SwiftUI 的 app、scene、controls、styles 和 widgets 更新,适合接着扩展可复用组件。

评论

GitHub Issues · utterances