<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>wwdc on liang2kl's Blog</title><link>https://yeshe.ng/blog/tags/wwdc/</link><description>Recent content in wwdc on liang2kl's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>liang2kl@outlook.com (Liang Yesheng)</managingEditor><webMaster>liang2kl@outlook.com (Liang Yesheng)</webMaster><copyright>from 2021, to infinity and beyond...</copyright><lastBuildDate>Wed, 08 Jun 2022 22:27:07 +0800</lastBuildDate><atom:link href="https://yeshe.ng/blog/tags/wwdc/index.xml" rel="self" type="application/rss+xml"/><item><title>WWDC22 Sessions 记录</title><link>https://yeshe.ng/blog/posts/2022/06/wwdc22/</link><pubDate>Wed, 08 Jun 2022 22:27:07 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/06/wwdc22/</guid><description>记录 WWDC22 上发布的新 API 和新技术</description><content>&lt;p>虽然 iOS 16 和 macOS 13 并没有太大的变化，但是 SDK（特别是 SwiftUI）更新的东西相当多，解决了很多以前开发中的痛点。&lt;/p>
&lt;p>目录（根据观看时间顺序，大致按照发布顺序）：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="#meet-swift-regex">Meet Swift Regex&lt;/a>&lt;/li>
&lt;li>&lt;a href="#meet-desktop-class-ipad">Meet desktop class iPad&lt;/a>&lt;/li>
&lt;li>&lt;a href="#the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/a>&lt;/li>
&lt;li>&lt;a href="#hello-swift-charts">Hello Swift Charts&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/a>&lt;/li>
&lt;li>&lt;a href="#compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/a>&lt;/li>
&lt;li>&lt;a href="#embrace-swift-generics">Embrace Swift generics&lt;/a>&lt;/li>
&lt;li>&lt;a href="#design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="meet-swift-regex">Meet Swift Regex&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110357/">https://developer.apple.com/videos/play/wwdc2022/110357/&lt;/a>&lt;/p>
&lt;p>Swift 5.7 新增了强类型的正则表达式类型，自动将字符串转化为需要的类型，支持编译器检查。另外还提供了 &lt;code>RegexBuilder&lt;/code> 框架，使用 SwiftUI 所使用的 result builder 语法来写正则表达式，非常具有表达力。编译器帮忙检查的 regex 和强类型的 capture 太香了。&lt;/p>
&lt;p>Regex literal，强类型静态正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex literals&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> digits = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d&lt;span style="color:#f92672">+/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// digits: Regex&amp;lt;Substring&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Regex Builder，使用 result builder 构建正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/02/2022 Payroll from employer $200.23&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/03/2022 Suspect A $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/03/2022 Ted&amp;#39;s Pet Rock Sanctuary $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/05/2022 Doug&amp;#39;s Dugout Dogs $33.27&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">RegexBuilder&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> fieldSeparator = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>s{&lt;span style="color:#ae81ff">2&lt;/span>,}&lt;span style="color:#f92672">|&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>t&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> transactionMatcher = Regex {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { &lt;span style="color:#f92672">/&lt;/span>CREDIT&lt;span style="color:#f92672">|&lt;/span>DEBIT&lt;span style="color:#f92672">/&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.date(.numeric, locale: Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>), timeZone: .gmt)) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OneOrMore {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NegativeLookahead { fieldSeparator }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CharacterClass.any
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.localizedCurrency(code: &lt;span style="color:#e6db74">&amp;#34;USD&amp;#34;&lt;/span>).locale(Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>))) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// transactionMatcher: Regex&amp;lt;(Substring, Substring, Date, Substring, Decimal)&amp;gt;}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Named captures，给各个 group 起名（返回的结果是一个 &lt;code>Tuple&lt;/code>，因此可以在编译期加上标识）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> regex = #&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;date&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">4&lt;/span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;middle&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>P{currencySymbol}&lt;span style="color:#f92672">+&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;currency&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>p{currencySymbol})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">/&lt;/span>#
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex&amp;lt;(Substring, date: Substring, middle: Substring, currency: Substring)&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="meet-desktop-class-ipad">Meet desktop class iPad&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10069/">https://developer.apple.com/videos/play/wwdc2022/10069/&lt;/a>&lt;/p>
&lt;p>针对 iPad 生产力问题作的改进，推出了更像 macOS 的 toolbar。不过，这远远不能解决 iPad 缺少足够 productive 的应用的问题。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-ipad.webp" alt="">&lt;/p>
&lt;h2 id="the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10054/">https://developer.apple.com/videos/play/wwdc2022/10054/&lt;/a>&lt;/p>
&lt;p>苹果终于改进了 SwiftUI 上视图导航的逻辑。简单来说就是以前只能傻傻地打开另一个视图而无法获取之前的视图的情况，现在可以获取并可以用程序控制当前所在的层级了。&lt;/p>
&lt;p>官方的示例代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">SwiftUI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Pushable stack&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PushableStack&lt;/span>: View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @State &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> path: [Recipe] = []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @StateObject &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> dataModel = DataModel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List(Category.allCases) { category &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Section(category.localizedName) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(dataModel.recipes(&lt;span style="color:#66d9ef">in&lt;/span>: category)) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationTitle(&lt;span style="color:#e6db74">&amp;#34;Categories&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .environmentObject(dataModel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>相比于之前 &lt;code>NavigationLink&lt;/code> 提供一个 &lt;code>View&lt;/code>，现在可以提供一个用于区分不同目标的值来代替（这样就可以记录栈上的内容）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，使用 &lt;code>navigationDestination()&lt;/code> 来返回对应的 &lt;code>View&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用新的 &lt;code>NavigationStack&lt;/code>，我们传递一个 &lt;code>path&lt;/code> 来获取当前导航栈上的所有 destination 所对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们可以通过更改 &lt;code>path&lt;/code> 来随意更改导航栈：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>path.removeAll()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，还可以通过 &lt;code>SceneStorage&lt;/code> 持久化当前的状态。&lt;/p>
&lt;h2 id="hello-swift-charts">Hello Swift Charts&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10136/">https://developer.apple.com/videos/play/wwdc2022/10136/&lt;/a>&lt;/p>
&lt;p>官方的图表框架，使用 SwiftUI 快速构建图表，非常简洁。比如，创建折线图：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>Chart(partyTasksRemaining) { element &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LineMark(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x: .value(&lt;span style="color:#e6db74">&amp;#34;Date&amp;#34;&lt;/span>, element.date, unit: .day),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> y: .value(&lt;span style="color:#e6db74">&amp;#34;Tasks Remaining&amp;#34;&lt;/span>, element.remainingCount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .symbol(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后效果很不错（也很苹果）：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart.webp" alt="">&lt;/p>
&lt;p>声明式语法真是太适合做图表了，matplotlib 虽然强大但是画个图挺麻烦的，在想着以后科研能不能用 Swift 画图（&lt;/p>
&lt;h2 id="whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10052/">https://developer.apple.com/videos/play/wwdc2022/10052/&lt;/a>&lt;/p>
&lt;p>除了上面提到的 Swift Charts 和 navigation 之外，这次 SwiftUI 的主要更新还有：&lt;/p>
&lt;h3 id="window-和-menu-bar-extras">Window 和 Menu bar extras&lt;/h3>
&lt;p>macOS 可以创建单独的、可以控制显示/关闭的窗口了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WindowGroup(&lt;span style="color:#e6db74">&amp;#34;Party Planner&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PartyPlannerHome()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Window(&lt;span style="color:#e6db74">&amp;#34;Party Budget&amp;#34;&lt;/span>, id: &lt;span style="color:#e6db74">&amp;#34;budget&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .keyboardShortcut(&lt;span style="color:#e6db74">&amp;#34;0&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，终于支持状态栏 app 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MenuBarExtra(&lt;span style="color:#e6db74">&amp;#34;Bulletin Board&amp;#34;&lt;/span>, systemImage: &lt;span style="color:#e6db74">&amp;#34;quote.bubble&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BulletinBoard()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .menuBarExtraStyle(.window)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="resizable-sheets">Resizable sheets&lt;/h3>
&lt;p>加入了去年 UIKit 新增的可变高度的 sheet：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.sheet(isPresented: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>presented) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDetents([.height(&lt;span style="color:#ae81ff">250&lt;/span>), .medium])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDragIndicator(.visible)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="其他">其他&lt;/h3>
&lt;p>其他的更新包括：&lt;/p>
&lt;ul>
&lt;li>macOS 上 &lt;code>Form&lt;/code> 新的外观&lt;/li>
&lt;li>支持多行的 &lt;code>TextField&lt;/code> 和限定行高范围&lt;/li>
&lt;li>iPad 支持 &lt;code>Table&lt;/code>&lt;/li>
&lt;li>可自定义的 toolbar&lt;/li>
&lt;li>可自定义的 layout&lt;/li>
&lt;/ul>
&lt;h2 id="whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10068/">https://developer.apple.com/videos/play/wwdc2022/10068/&lt;/a>&lt;/p>
&lt;p>现在基本不写 UIKit 了，这部分就没怎么记录。比较有意思的是，现在创建 &lt;code>UICollectionView&lt;/code> 和
&lt;code>UITableView&lt;/code> 可以用 SwiftUI 来写 cell 的 UI 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>cell.contentConfiguration = UIHostingConfiguration {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Image(systemName: &lt;span style="color:#e6db74">&amp;#34;wand.and.stars&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Like magic!&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title2).bold()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(Color.purple)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其他的更新，如新的 toolbar，到暑假可以用来改进一下 Whiz Reader（不过可能用 SwiftUI 重写而不是继续用 UIKit）。&lt;/p>
&lt;h2 id="compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10056/">https://developer.apple.com/videos/play/wwdc2022/10056/&lt;/a>&lt;/p>
&lt;p>支持自定义布局应该是本次 SwiftUI 最重要的更新了。现在我们可以访问到一些以前获取不到的信息来放置子视图了。&lt;/p>
&lt;h3 id="grid">Grid&lt;/h3>
&lt;p>在介绍自定义布局之前，首先介绍了新增的 &lt;code>Grid&lt;/code> 类型，可以构建静态的对齐与网格的布局。&lt;/p>
&lt;p>与之前的 &lt;code>LazyHGrid&lt;/code> 和 &lt;code>LazyVGrid&lt;/code> 不同，&lt;code>Grid&lt;/code> 不是懒加载的，因此高/宽是确定的，适合于静态 grid 的构建。&lt;/p>
&lt;p>与 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 的组合相比，&lt;code>Grid&lt;/code> 的各行/列可以自动根据最大元素调整大小，而 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 不行。比如，现在可以实现这样一个列表：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-grid.webp" alt="">&lt;/p>
&lt;p>左侧的名称一栏自动将宽度调整到适合于最大的 &lt;code>Goldfish&lt;/code>，右侧同理。&lt;/p>
&lt;blockquote>
&lt;p>在以前，使用 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 想要实现各列大小一致，只能给各列指定固定的大小。比如，&lt;a href="https://github.com/treehollow/treehollow-v3-ios/blob/main/Source/Hollow/View/Hierarchy/Hollow/Content/HollowCommentContentView.swift">树洞的评论界面&lt;/a>大概是这样实现的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(comments) { comment &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .fixedSize(.horizontal, &lt;span style="color:#ae81ff">20&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当 &lt;code>comment.name&lt;/code> 过长时，就会变成两行，非常影响体验。&lt;/p>
&lt;/blockquote>
&lt;h3 id="自定义布局">自定义布局&lt;/h3>
&lt;p>自定义布局对应一个新的协议：&lt;code>Layout&lt;/code>。&lt;/p>
&lt;p>&lt;code>Layout&lt;/code> 通过两个函数参与布局过程：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">sizeThatFits&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) -&amp;gt; CGSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">placeSubviews&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">in&lt;/span> bounds: CGRect,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一个函数 &lt;code>sizeThatFits()&lt;/code> 通过返回一个大小来告诉父 &lt;code>View&lt;/code> 我们需要多少空间；第二个函数 &lt;code>placeSubviews()&lt;/code> 将所有的子 &lt;code>View&lt;/code> 放置在合适的位置（使用我们在 &lt;code>sizeThatFits&lt;/code> 中要求的空间）。&lt;/p>
&lt;h4 id="sizethatfits">sizeThatFits&lt;/h4>
&lt;p>我们先来看 &lt;code>sizeThatFits()&lt;/code>。它有三个参数：&lt;/p>
&lt;ul>
&lt;li>&lt;code>proposal&lt;/code>：父 &lt;code>View&lt;/code> 所期望这个布局的大小。我们（可以）根据 &lt;code>proposal&lt;/code> 的大小来决定我们最终要占据多少空间，当然也可以忽略&lt;/li>
&lt;li>&lt;code>subviews&lt;/code>：布局中所有的子 &lt;code>View&lt;/code>。这并不是 &lt;code>View&lt;/code> 本身，而是它的“代理”（proxy），仅给我们提供必要的信息和接口&lt;/li>
&lt;li>&lt;code>cache&lt;/code>：可用来缓存中间计算结果&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>关于 &lt;code>proposal&lt;/code>，官方文档是这样介绍的：&lt;/p>
&lt;p>A proposed size for the subview. In SwiftUI, views choose their own size, but can take a size proposal from their parent view into account when doing so.&lt;/p>
&lt;p>可以看到，&lt;code>proposal&lt;/code> 是父视图对子视图大小的“建议”。父视图在进行布局时，将建议的大小传入每个子视图，子视图经过计算返回最终的大小（可以是 &lt;code>proposal&lt;/code> 的大小，也可以不是），父视图最终根据子视图返回的大小来进行布局的计算（而不是它所认为的大小，即 &lt;code>proposal&lt;/code>）。&lt;/p>
&lt;/blockquote>
&lt;p>我们以官方的例子为例。这个例子实现了一个 &lt;code>EqualWidthHStack&lt;/code>，有几个需求：&lt;/p>
&lt;ul>
&lt;li>水平布局&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 大小相同&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 的大小都等于最大的子 &lt;code>View&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>对于这个 layout，&lt;code>sizeThatFits()&lt;/code> 如下：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-sizethatfits.webp" alt="">&lt;/p>
&lt;p>原理很简单：计算子 &lt;code>View&lt;/code> 的最大的 size，加上之间的 spacing，即是布局需要的大小。我们通过 &lt;code>LayoutSubview&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 获取每个子 &lt;code>View&lt;/code> 的大小，然后计算最大值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">maxSize&lt;/span>(subviews: Subviews) -&amp;gt; CGSize {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CGSize(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> width: max(currentMax.width, subviewSize.width),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> height: max(currentMax.height, subviewSize.height))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> maxSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>sizeThatFits()&lt;/code> 中，我们忽略了 &lt;code>proposal&lt;/code>，这样在父视图的任何大小下这个布局所占的空间都是一样的。如果需要更灵活的布局，如空间不足时自动缩小，可以根据不同的 &lt;code>proposal&lt;/code> 返回不同的大小。&lt;/p>
&lt;h4 id="placesubviews">placeSubviews&lt;/h4>
&lt;p>&lt;code>placeSubviews()&lt;/code> 相较于 &lt;code>sizeThatFits()&lt;/code> 多了一个参数 &lt;code>bounds&lt;/code>，其余参数均相同；我们通过调用 &lt;code>LayoutSubview&lt;/code> 的方法来放置子视图。其实完全可以理解为两个函数是同一个函数的两个部分，&lt;code>sizeThatFits()&lt;/code> 用于计算视图大小，&lt;code>placeSubviews()&lt;/code> 用我们计算好的大小来放置视图；只是因为上层调用是两个不连续的过程，才将它们分开为两个函数。&lt;/p>
&lt;p>因此，这两个函数一般具有部分相同的逻辑，比如上面的 &lt;code>maxSize()&lt;/code> 和 &lt;code>spacing()&lt;/code>，最好将其写到一个新的函数中以便复用，或者直接将中间结果放到 &lt;code>cache&lt;/code> 中（待考证）。&lt;/p>
&lt;p>实现 &lt;code>EqualWidthHStack&lt;/code> 的 &lt;code>placeSubviews()&lt;/code> 很简单，我们只需要通过相同的方法计算 &lt;code>maxSize&lt;/code> 和 &lt;code>spacing&lt;/code>，然后将每个 &lt;code>subView&lt;/code> 放置在合适的位置就可以了。这个过程通过 &lt;code>LayoutSubView&lt;/code> 的 &lt;code>place()&lt;/code> 方法完成。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-placesubviews.webp" alt="">&lt;/p>
&lt;h4 id="传递额外信息">传递额外信息&lt;/h4>
&lt;p>这种方式有一个问题：传入的 &lt;code>subviews&lt;/code> 只是视图的代理，不带有视图的信息。如果我们需要传递子视图的额外信息用于布局的计算，官方提供了一种（强类型的）方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Rank&lt;/span>: LayoutValueKey {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> defaultValue: Int = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MyLayout(&lt;span style="color:#ae81ff">1.&lt;/span>..&lt;span style="color:#ae81ff">10&lt;/span>) { rank &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>rank&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .layoutValue(key: Rank.&lt;span style="color:#66d9ef">self&lt;/span>, value: rank)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用下标运算符，我们可以从 &lt;code>subview&lt;/code> 中获取对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> ranks = subviews.map { subview &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subview[Rank.&lt;span style="color:#66d9ef">self&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样就实现了额外信息的传递。&lt;/p>
&lt;h3 id="自适应布局">自适应布局&lt;/h3>
&lt;p>有意思的是，官方还实现了一个能够根据可用空间的大小自动调整布局的布局 &lt;code>ViewThatFits&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>ViewThatFits {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EqualWidthHStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当空间足够时，选择第一个 &lt;code>EqualWidthHStack&lt;/code>；空间不够时，选择所需空间更少的 &lt;code>VStack&lt;/code>。&lt;/p>
&lt;p>其实这个非常 fancy 的布局的实现应该很简单：根据 &lt;code>EqualWidthHStack&lt;/code> 和 &lt;code>VStack&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 的返回值，按顺序选择能够在可用空间内放置的一个。当然，我们没办法在自定义排布中选择哪些子视图不显示，因此是没办法完美地手动实现一个类似的容器的（可以通过不给子视图分配空间来实现，不过这样子视图仍然会加载）。&lt;/p>
&lt;h3 id="anylayout">AnyLayout&lt;/h3>
&lt;p>最后，官方提供了一个 type-erased 的 &lt;code>Layout&lt;/code>，即 &lt;code>AnyLayout&lt;/code>，可以用于动态改变布局类型：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> layout = isThreeWayTie ? AnyLayout(HStack())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : AnyLayout(MyRadialLayout())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>layout {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(pets) { pet &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Avatar(pet: pet)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.animation(...)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，由于 &lt;code>layout&lt;/code> 中的 &lt;code>View&lt;/code>（在这里是若干个 &lt;code>Avatar&lt;/code>）在不同的布局下具有相同的 view identity，因此布局变化前后子视图是不变的。通过这一点，我们甚至可以为不同布局的变化加上动画。&lt;/p>
&lt;h2 id="swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10137/">https://developer.apple.com/videos/play/wwdc2022/10137/&lt;/a>&lt;/p>
&lt;p>Swift Charts 框架的进阶介绍，主要介绍了一些自定义方法。&lt;/p>
&lt;p>增加标注：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-annotation.webp" alt="">&lt;/p>
&lt;p>指定图表颜色：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-color.webp" alt="">&lt;/p>
&lt;p>自定义刻度：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-axis.webp" alt="">&lt;/p>
&lt;p>自定义图表区域：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-area.webp" alt="">&lt;/p>
&lt;p>在图表上添加一层 overlay，可用于交互：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy.webp" alt="">
&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy-2.webp" alt="">&lt;/p>
&lt;h2 id="embrace-swift-generics">Embrace Swift generics&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110352/">https://developer.apple.com/videos/play/wwdc2022/110352/&lt;/a>&lt;/p>
&lt;p>介绍了 Swift 范型的更新，主要针对 protocol。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Farm&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feedAll&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animals: [any Animal]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> animal &lt;span style="color:#66d9ef">in&lt;/span> animals {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> feed(animal)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上面的代码中有几个关键点：&lt;/p>
&lt;ul>
&lt;li>协议限制在 Swift 5.7 中可以用 opaque type，即 &lt;code>some&lt;/code> 关键字表示&lt;/li>
&lt;li>可以用 &lt;code>type(of:)&lt;/code> 来获取确定的具体类型&lt;/li>
&lt;li>在 Swift 5.7 中可使用 &lt;code>any&lt;/code> 实现针对 protocol 的类型消除（type erasure）&lt;/li>
&lt;/ul>
&lt;p>&lt;code>any&lt;/code> 非常实用。之前，数组只能储存同一具体类型的数据，不能储存遵循相同协议而类型不同的数据（主要原因应该是内存大小不同）；在 Swift 5.7 中，使用 &lt;code>any&lt;/code> 可以储存遵循某协议的任意类型的数据。现在，数组就不一定直接储存对象本身了。如果对象大小合适（这里不太清楚“合适”的含义，估计是指针大小以内？），可以直接存到数组里，否则存在外面：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-type-erasure.webp" alt="">&lt;/p>
&lt;p>这种方式有点像存了一堆指针，但是又不完全是（较小的对象可以直接放在数组内），可能是为了减少虚函数查表的开销？&lt;/p>
&lt;p>另外，&lt;code>any&lt;/code> 类型可以用于 &lt;code>some&lt;/code>，具体类型的获取在运行时完成。这种转换叫做“opening boxes”，即“打开盒子看究竟是什么动物”：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-opening-boxes.webp" alt="">&lt;/p>
&lt;p>最后，官方建议：&lt;/p>
&lt;blockquote>
&lt;p>Write &lt;strong>some&lt;/strong> by default&lt;/p>
&lt;/blockquote>
&lt;p>道理很简单，直接调用带 &lt;code>some&lt;/code> 参数的函数实际上在编译期是确定了类型的，而 &lt;code>any&lt;/code> 在编译期去掉了具体类型的信息，在运行期才能确定，相比之下效率较低。&lt;/p>
&lt;h2 id="design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110353/">https://developer.apple.com/videos/play/wwdc2022/110353/&lt;/a>&lt;/p>
&lt;p>在上面 Embrace Swift generics 的基础上，进一步介绍使用新的语言特性设计协议。&lt;/p>
&lt;h3 id="type-erasure">Type erasure&lt;/h3>
&lt;p>和上面类似的例子。当我们使用类型消除时，所有类型变成“上界”（不太明白这个名词的意思&amp;hellip;），与之相关的所有泛型都变成对应的上界。&lt;/p>
&lt;p>比如，&lt;code>any Animal&lt;/code> 调用 &lt;code>produce()&lt;/code> 的结果类型为 &lt;code>any Food&lt;/code>：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics.webp" alt="">&lt;/p>
&lt;p>但是，反过来，不能够将某一个符合 associatedtype 的类型作为参数传入一个上界类型的函数中。&lt;/p>
&lt;p>比如，&lt;code>Crop()&lt;/code> 不能作为 &lt;code>any Animal&lt;/code> 的 &lt;code>eat()&lt;/code> 的参数：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics-2.webp" alt="">&lt;/p>
&lt;p>这里的关键在于，一个具体类型总是可以安全地转换为 &lt;code>any&lt;/code>，而 &lt;code>any&lt;/code> 不能反过来安全地转换为某一个具体的类型。&lt;/p>
&lt;h3 id="hide-implementation-details">Hide implementation details&lt;/h3>
&lt;p>有时候，我们不希望暴露太多实现细节，比如下面的这个例子：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter.webp" alt="">&lt;/p>
&lt;p>我们希望使用 &lt;code>LazyFilterSequence&lt;/code> 来提高性能，然而并不希望将这个信息暴露在 API 中。Swift 为了解决这个问题，增加了一个叫做 primary associated types 的语法：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter-opaque.webp" alt="">&lt;/p>
&lt;p>Primary associated types 大致的意思是，将 protocol 中与实现细节相关的 associatedtype 使用尖括号暴露出来，从而能够使用 &lt;code>some&lt;/code> 或者 &lt;code>any&lt;/code> 来表示满足这个 associatedtype 要求的类型。&lt;/p>
&lt;p>比如，标准库中的 &lt;code>Collection&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Collection&lt;/span>&amp;lt;Element&amp;gt;: Sequence {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Element
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="关于-typeof">关于 &lt;code>type(of:)&lt;/code>&lt;/h3>
&lt;p>看完视频最主要的疑问是：&lt;code>type(of:)&lt;/code> 究竟是完全由运行时决定的，还是有编译器参与的。比如上面的例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器知道 &lt;code>some Animal&lt;/code> 是一个具体的、遵循 &lt;code>Animal&lt;/code> 的类型（包括运行时才能确定类型的 &lt;code>any Animal&lt;/code>），因此可以确定 &lt;code>type(of: animal).Feed&lt;/code> 的类型就是具体类型的 &lt;code>Feed&lt;/code>，上面的代码得以通过编译。&lt;/p>
&lt;p>但是，脱离了 &lt;code>some&lt;/code> 的语境，&lt;code>any&lt;/code> 就没有这种机制了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> animal: any Animal = Cow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// error: type of expression is ambiguous without more context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// let crop = type(of: animal).Feed.grow()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// ~~~~~~~~~~~~~~~~~^^^^~~~~~~~&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器实际上通过 &lt;code>some&lt;/code> 给 &lt;code>any&lt;/code> 提供了便利，即虽然不能在编译期知道具体的类型，但是可以保证 associatedtype 的正确性，这也是上面所说的“Open boxing”实现的效果。要想利用这种便利，我们需要使用带有 &lt;code>some&lt;/code> 参数的函数。&lt;/p></content></item></channel></rss>