2022 Apple 在 Swift、SwiftUI、效能、Xcode 以及 App Service 等技術做了哪些新增和更新

語言: CN / TW / HK

第一天

今年是 WWDC 的第39個年頭了。今年的 WWDC.playground 活動()是 SwiftGG、T 沙龍和老司機技術一起會和社群開發者們一起聊聊這次 WWDC。WWDC.playground 活動在節日期間每天都會有直播,我會和 61、13 他們參加 6月11日晚上8點那場直播。

下面我整理了一份今年 WWDC 的指南,也算提供個方便的入口吧。

  1. WWDC22 直播地址微博直播WWDC22 YouTube 地址
  2. Apple WWDC22 頁面
  3. Apple WWDC22 指南
  4. Apple Developer app 觀看 Session 的 Apple 出的 App。
  5. Session 網頁版
  6. Digital Lounge 註冊感興趣的主題,到時候就可以和 Apple 工程師在 Slack 上一起看 Session,交流。
  7. Labs 可以獲得和 Apple 專家一對一指導。6號 keynote 完後就可以開始預約。
  8. Beyond WWDC22 和去年一樣,這裡是 Apple 製作的世界各地的社群活動。
  9. weak self Discord WWDC22 Keynote Watch Party 全球最多聽眾的 iOS 中文 Podcast 之一 weak self 的活動。
  10. Swiftly Rush WWDC22
  11. iOS Feeds 的 WWDC 2022 新聞聚合
  12. WWWDC.io App 社群的看 Session 的 App。
  13. Keynote 後的 Platforms State of the Union 這個主題是對後面一週 Session 的總結,開發者可以重點關注下。
  14. WWDC Notes 匯聚了大家的 Session 筆記,可以快速看到各個 Session 的重點。
  15. Technologies 這裡是 Apple 框架 API 分類地址,看完 Session 可以直接在這裡找對應 API 的更新。還有個網站 Apple Platform SDK API Differences 會列出新 SDK 裡有哪些框架更新了。
  16. Apple Design Awards 提名作品

Apple Design Awards 提名作品,我先列幾個我喜歡的:

  1. procreate
  2. Wylde Flowers
  3. 籠中窺夢
  4. Gibbon: Beyond the Trees
  5. Vectornator: Design Software
  6. Wylde Flowers
  7. Behind the Frame
  8. MD Clock - Live in the present
  9. 專注麵條
  10. Townscaper

第二天

01.png

今天最讓我印象深刻是 M2、Lock screen widgets、Stage manager、Swift Charts、WeatherKit、SwiftUI Navigation API、只要一個 1024x1024 App Icon、Sticky headers on Xcode scrolling、Xcode View Debugger 可以用於 SwiftUI 了,還有 iOS 16 原生的支援 Nintendo Switch Pro 手柄了。

後面我將更多內容使用點對點的分發,可以用 Planet 關注,我的 IPNS 是:k51qzi5uqu5dlorvgrleqaphsd1suegn8w40xwhxl0bgsyxw3zerivt59xbk74

Keynote 要點:

  • iOS 16
  • new lock screen
  • live activities
  • extend focus to lock screen
  • forcus filter for apps
  • dictation improvements
  • live text in video
  • visual lookup
  • maps
    • multistop routing
    • transit(add card to wallet)
    • new details
    • lookaround api
  • iCloud shared photo library
  • persanalized spatial audio
  • quick notes on iPhone
  • fitness app without watch
  • messages
    • edit messages
    • delete messages
    • mark as unread
    • share play
  • pay
    • tap to pay on iPhone
    • order tracking
  • carplay
    • widgets
    • more personalization
    • multi-screen
  • safety check
    • quickly remove access for others
  • home
    • introduce matter as new standard
    • redesign of app
  • M2
  • 15.8 trillion operations per seconds
  • 10-core GPU
  • macbook air and macbook pro 13”
  • better and faster
  • silent design
  • fast charge
  • new colors
  • magsafe
  • audio jack
  • macOS Ventura
  • improved spotlight
  • undo send and more
  • shared tab groups
  • passkeys
  • desk view
  • stage manager
  • continuity for facetime
  • use iPhone as camera on macbook
  • iPadOS 16
  • weather app
  • WeatherKit
  • collaborations api
  • freeform board
  • stage manager
  • WatchOS 9
  • four new watch faces
  • new ShareKit api
  • improved metrics for running
  • heart rate zones
  • create custom workouts

重要的幾個資訊:

大讚的庫:

好用的功能和元件:

一些方便上手的例子:

一些感興趣的 Session:

第三天

02.jpg 03.jpg

WWDC.playground 很精彩,怎麼感覺昨天的 WWDC.playground 像是聽了一期楓言楓語呢。預感 11 號可能會變成為一期 weak self 呢。

昨天老司機還整理了份 WWDC22 Session 觀看介紹的列表

Apple 出的內容看不夠的話,可使用 Follow WWDC 2022 News! 來看最新的 WWDC 相關的社群文章。

下面是我今天的一些記錄。

Xcode

程式碼補全的更新。以前多個可選引數的體驗很差,這次輸入引數比如 frame 裡的 maxWidth,會只顯示當前要補全的引數。而且速度快了很多。

以前是編完原始碼再生成 module,然後 link編好的檔案,最後再 link。現在整個過程改成並行執行,同時 link 還快了兩倍。結果是比以前快了25%,核越多效果越明顯。還有可視覺化整個過程。

多平臺以前是多個 tagets,現在是在一個 target 裡管理。

Hangs 是官方線上主執行緒被卡了的檢查工具,在 Organizer 裡檢視對應問題堆疊也很方便。

當然最愛的還是 sticky headers,秒殺其它編輯器 (雖然我還是覺得 Emacs 最好,由於會暴露年齡,一般我都不說)。

還有記憶體也好了很多,總體來說,這次 Xcode 更新很棒。

完整 Xcode release notes

WidgetKit

WidgetKit 將 WatchOS 上的 Circular、Rectangle 還有 Inline 帶到了 iOS 和其他平臺。

WeatherKit

安全方便獲得使用者位置資訊,只用於天氣。

VisionKit

Live Text API,感覺這類庫都是為了以後出眼鏡做鋪墊的。

macOS

macOS 支援window,menuBar也支援了。

Swift

distrubuted actor 更安全,還可以在裝置間(本地裝置<->本地裝置本地裝置<->伺服器)進行通訊保護。

泛型新語法 some 和 any 關鍵字寫起來真的簡化了很多。

Swift 的更新了什麼,除了 Session 外,還可以參看 Paul Hudson 這篇文章 What’s new in Swift 5.7 ,還有 Donny Wals 的這篇 What’s the difference between any and some in Swift 5.7?

SwiftUI

SwiftUI裡沒有用屬性包裝的屬性也能夠和檢視變化綁定了。

關於 SwiftUI 的更新,Paul Hudson 寫了很多例子 What’s new in SwiftUI for iOS 16

Reda Lemeden 整理了 WWDC22 SwiftUI 的所有相關內容 SwiftUI @ WWDC 2022 。可見社群對 SwiftUI 熱情依然是最高的。

SPM

Swift Package Plugin,本來用其他語言,比如 ruby 、python 或 shell 做的事情,現在可以通過 Swift 語言來完成了,寫的 plugin 還可以方便的在 Xcode 中使用。

虛機

使用 Virtualization 框架,享受 Rosetta 2 的優勢,執行 x86-64 Linux 系統。

Apple 出虛機可執行 Linux 系統這點可以看得出 Apple 對開源的擁抱,原因還有一點是 Swift 也可以用在 Linux 伺服器上了,Apple 用心良苦,也是想讓開發者用本打算買其它硬體的錢來買 Apple 的硬體吧,更好的榨乾 Apple 硬體過於優秀的效能,如同新出 Stage Manager 通過投到大屏來榨乾 M1 的 iPad 效能。 不光是這樣,還有檔案,也就是儲存裝置也只需要一份了,更方便,還有蘋果特有的 Trackpad 和 Magic mouse 也能夠用於 Linux 系統中。

虛機執行 Linux 和 macOS 的區別是,啟動 Linux 使用的是 EFI Boot Loader 來載入 Linux 檔案,VirtioGraphicDevice 進行 Linux 系統圖形介面的設定和渲染。使用Rosetta 執行 Linux 系統,執行 Linux 就是比其它虛機要快。

介紹的 session Create macOS or Linux virtual machines ,程式碼說明 Running GUI Linux in a virtual machine on a Mac,相關主題 Virtualization

第四天

04.png

今晚五神會現身 WWDC.playground 。內容涉及 SwiftUI 和 AR,不要錯過。

今日零散記錄

從 Apple 推出 WeatherKit 可以看出,Apple 喜歡把關鍵和有想象空間盈利價值的技術掌握在自己手上,WeatherKit 提供大量資料,包括分鐘、小時、每日預報,還有提前警報,這些資訊的商業價值本就很大。

今天看了 WeatherKit、Swift Chart 還有 SwiftUI 的 Layout,感覺 Apple 的介面設計能力很值得學習,可能具備了這些能力才能更好地溝通。

swift-algorithms 可以使用 .indexed() 來替代 zip。

Federico Zanetello 對 Platforms State of the Union 這個 Session 做的筆記

應用層面,今天還有好多 Swift Chart 的介紹。

Layout

Grid、Layout、ViewThatFits、AnyLayout,特別是 Grid 還統一了 HStack 和 VStack。這些佈局方式,讓先前複雜的要藉助 GeometryReader,且容易出錯的佈局有了更易的寫法。Layout 協議可以為 layout 建立自定義屬性,另外佈局計算也會被快取。

Link

Link fast: Improve build and launch time 詳細講了 Apple 今年怎麼改進了 link,思路很棒,很值得學習。

Static linking 和 Dynamic linking ,也就是靜態連結和動態連結。

靜態連結就是連結各個編譯好的原始檔以及連結原始檔和編譯好的庫檔案,通過將函式名放到符號表,連結新檔案時確定先前是否有包含的 undefined 符號,給函式的資料指令分配地址,最後生成一個有 TEXT、DATA、LINKEDIT 段的可執行檔案。

今年 Apple 通過利用多核優勢讓靜態連結快了兩倍。

具體做法是,並行的拷貝檔案內容。並行構建 LINKEDIT 段的各個不同部分。並行改變 UUID 計算和 codesigning 雜湊。然後是提高 exports-trie 構建器的演算法。使用最新的 Crypto 庫利用硬體加速的優勢加速 UUID 計算。提高其它靜態庫處理演算法庫,debug-notes 生成也更快了。

Apple 推薦靜態庫最佳實踐是:

使用 -all_load-force_load 可以讓 .a 檔案像 .o 檔案那樣並行處理,不過開啟這個選項需要先處理重複的符號。另外一個副作用是會將一些被判斷無用的程式碼也被連結進來,使包體變大,因此開啟之前可以先使用靜態分析工具分析處理,這個過程定期做就行,不用放到每次編譯過程中。演講者推薦使用 -dead_strip 選項,但是這樣做並沒有真實去掉費程式碼,以後這些程式碼還是會被編譯分析,如果只是暫時不用,可以先註釋掉。

使用 -no_exported_symbols 選項。連結器生成的 LINKEDIT 段的一部分是 exports trie,這是一個字首樹,對所有匯出的符號名稱、地址和標誌進行編碼。動態庫 是會匯出符號的,但執行的二進位制檔案其實是不用這些符號的,因此可以用 -no_exported_symbols 選項來跳過 LINKEDIT 中 trie 資料結構的建立,這樣連結起來就快多了。如果程式匯出符號是一百萬個,這個選項就可以減少 2 到 3 秒的時間。但需要注意的是,如果要載入外掛連結回主程式就需要所有的匯出的 trie 資料,無法用這個選項。

另外一個是 -no_deduplicate 選項。先前 Apple 給連結器加了個 pass 用來合併函式的指令相同,函式名不相同,這個 pass 會對每個函式的指令進行遞迴雜湊,用這種方式來找重複指令,這樣做比較費 CPU,由於除錯時其實是不需要關注包大小,因此可以加上 -no_deduplicate 選項來跳過這個 pass。

這些選項在 Xcode 的 Other Linker Flags 裡進行設定即可。

動態庫也就是 dylib,其它平臺就是 DSO 或 DLL。 動態連結器不是將程式碼從庫裡考到主二進位制裡,而是記錄某種承諾,記錄從動態庫中使用符號名稱,還有庫路徑。這樣做好處就是好複用動態庫,不用拷貝多份。虛擬記憶體看到多程序使用相同動態庫,就會重新給這個動態庫用相同的實體記憶體頁。

動態庫好處是構建快了,啟動載入慢了,多個動態庫不光要載入,還要在啟動時連結。也就是把連結成本從本地構建換到了使用者啟動時。動態庫還有個缺點是基於動態庫的程式會有更多的 dirty 頁,因為靜態連結時會把全域性資料放到主程式同一個 DATA 頁中,動態庫的話,每個都在自己的 DATA 頁中。

動態庫工作的原理是,可執行的二進位制會有不同許可權的段,至少會有 TEXT、DATA 和 LINKEDIT。分段總是作業系統頁大小的倍數。TEXT 段有執行的許可權,CPU 可以將頁上的位元組當做機器程式碼指令。執行時,dyld 會根據每個段許可權將可執行檔案 mmap() 到記憶體,這些段是頁大小和頁對齊的,虛擬記憶體系統可以直接將程式或動態庫檔案設定為 VM 範圍的備份儲存。在這些頁的記憶體訪問前是不會被載入到 RAM 裡,就會觸發一個頁 fault,導致 VM 去讀取檔案的子範圍,將記憶體填充到需要 RAM 頁中。光對映不夠,還要用某種方式“wired up”或綁到動態庫上。比如要呼叫動態庫上的某個函式,會轉換成呼叫 site,呼叫 site 成為一個在相同 TEXT 段合成的 sub 的呼叫,相對地址在構建時就知道了,就意味著可以正確的形成 BL 指令。這樣做的好處是,stub 從 DATA 載入一個指標並跳到對應的位置,不用在執行時修改 TEXT 段,dyld 只在執行時改 DATA 段。dyld 所進行的修改很簡單,就是在 DATA 段裡設定了一個指標而已。

當 dyld 或應用程式的指標指向自己時要 rebase,ASLR 使 dyld 以隨機地址載入動態庫,內部指標不能在構建時設定,dyld 在啟動時 rebase 這些指標,磁碟上,如果動態庫在地址零出被載入,這些指標包含它們的目標地址。LINKEDIT 需要記錄的就是每個重定位的位置。然後,dyld 只需將動態庫的實際載入地址新增到每個 rebase 位置。還有種修改方式是繫結,繫結就是符號引用,符號儲存在 LINKEDIT 中,dyld 在動態庫的 exports tire 中找實際地址,然後 dyld 將該值儲存在繫結指定的位置。

今年 Apple 釋出了一個新的修改方式 chained fixups。較前面兩種的優勢就是可以使 LINKEDIT 更小。新格式只儲存每個 DATA 頁中第一個 fixup 位置和一個匯入的符號列表。其它資訊編碼到 DATA 段。iOS 13.4 就開始支援了。

下面先說下 dyld 原理介紹。

dyld 從主可執行檔案開始,解析 mach-o 找依賴動態庫,對動態庫進行 mmap()。然後對每個動態庫進行遍歷並解析 mach-o 結構,根據需要載入其它動態庫。載入完畢,dyld 會查詢所有需要繫結符號,並在修改時使用這些地址。最後修改完,dyld 自下而上執行初始化程式。先前做的優化是隻要程式和動態庫,dyld 很多步驟都可以在首次啟動時被快取。

今年 Apple 做了更多的優化,這個優化叫 page-in linking,就是 dyld 在啟動時做的 DATA 頁面修改放到 page-in 時,也可以理解為懶修改。以前,在 mmap() 區域的某些頁面中第一次使用某些地址會觸發核心讀入該頁面。現在如果它是一個數據頁,核心會應用改頁需要的修改。這種機制減少了 dirty 記憶體和啟動時間。意味著 DATA_CONST 也是乾淨的,可以像 TEXT 頁一樣被 evicted 和重新建立,以減少記憶體壓力。需要注意的是 page-in linking 只用於啟動,dlopen() 不支援。你看,Apple 優化啟動的思路也是按需載入。

Apple 還提供了追蹤 dyld 執行情況的 dyld_usage 工具。檢查磁碟和 dyld 快取中的二進位制檔案的 dyld_info 工具。

今日推薦 Session

除了 link 外,還有 Meet distributed actors in Swift 也是比看的,Mike Ash 和 Doug Gregor 一年的心血就在這了。

第五天

05.png

效能

效能的 Improve app size and runtime performance Session 值得一看。

今年蘋果通過更有效的檢查 Swift 協議,使 OC 訊息傳送呼叫更小,使 autorelease elision 更快更小這幾個個方面來讓 App 體積更小,效能更高。

Swift 協議檢查。

一個協議通過 as 操作符檢查傳遞值是否符合協議,這種檢查會在編譯器的構建時間被優化掉,所以往往需要在執行時藉助之前計算協議檢查元資料來看物件是否真的符合了協議。一些元資料是在編譯時建的,但還有很多元資料只能在啟動時建立,特別是使用泛型時。協議多了,會增加耗時,差不多會多一半啟動時間。

今年 Apple 推出新的 Swift 執行時,可以提前計算 Swift 協議元資料,作為 App 可執行檔案和它在啟動時使用的任何動態庫的 dyld 閉包的一部分。這個是在系統上的,因此,只要是使用了今年最新系統的 App 都會享受這個優化,可以理解為,新系統上啟動老 App 也會快些。

訊息傳送。

Xcode 14 中新的編譯器和連結器已經將 ARM64 的訊息傳送呼叫從 12 位元組減少到 8 位元組。因此如果你的 App 都是 OC 程式碼的話,使用 Xcode 14 編出來的二進位制檔案可以少 2%。老系統也有效。

使用 objc_stubs_small 選項可以只優化大小,獲得最大的大小優化。objc_msgSend 調動有 8 個位元組指令,也就是2個指令是專門用來準備 selector 的,對於任何特定的 selector,總是相同的程式碼,由於始終是相同的程式碼,那麼就可以對其共享,每個 selector 只 emit 一次,而不是每次傳送訊息時都 emit。共享這段程式碼地方是一個叫 selector stub 的函式。

ARC 會在編譯器插入大量的 c 的 retain/release 函式呼叫。這些呼叫遵守平臺應用二進位制介面(ABI)所定義的 c 語言 call convention。也就意味著我們要更多程式碼來完成這些呼叫,用來傳遞正確暫存器的指標。Apple 今年推出了自定義的 call convention 根據指標位置,適時使用正確變數而不用移動它,從而擺脫了呼叫裡的多餘程式碼。Apple 果然是堅持使用者體驗優先,為了更好體驗不惜修改 c 的 ABI。

autorelease elision 。

App 今年對 objc 執行時進行了修改,使 autorelease elision 更小更快。deployment target 為 iOS 16 今年新系統時才可享用哦。

Apple 怎麼做的呢?

ARC 在呼叫方插入一個 retain,在被呼叫的函式中插入一個 release。當我們返回我們的臨時物件時,我們需要在函式中先釋放它,因為它要離開 scope。在它還沒有任何其它引用時還不能這麼做,不然返回前他就會被銷燬。Apple 現在使用一個新的 convention ,讓其可以返回臨時物件。做法是當返回一個自動釋放值,編譯器會發出一個特殊標記,這個標記會告訴執行時這是符合自動釋放條件的。它的後面是 retain,我們會在後面執行。獲取返回地址,也就是一個指標,將它先儲存起來,然後離開執行時的自動釋放呼叫。在執行時,可以將保留時得到的指正和先前做自動釋放時儲存的指標進行比較,這樣標記指令不再是資料之間的比較,比較指標記憶體訪問少。比較成功就可以省去 autorelease/retain。

autorelease elision 的優化同樣也可以減少 2% 大小。感謝 Apple 為了使用者和開發者 OKR 的付出。

SwiftUI

new navigation api,看完感覺我做的小冊子還有幻燈應用要花些時間好好改改了。

接下來,有活幹了。

WWDC.playground

明天的 WWDC.playground 嘉賓有謎底科技和 weak self,歡迎來捧場。

下面是按分類做的記錄:

Swift

String Index 大升級 String Index Overhaul

參考

Regex

標準庫多了個 Regex<Output> 型別,Regex 語法與 Perl、Python、Ruby、Java、NSRegularExpression 和許多其他語言相容。可以用 let regex = try! Regex("a[bc]+")let regex = /a[bc]+/ 寫法來使用。SE-0350 Regex Type and Overview 引入 Regex 型別。SE-0351 Regex builder DSL 使用 result builder 來構建正則表示式的 DSL。SE-0354 Regex Literals 簡化的正則表示式。SE-0357 Regex-powered string processing algorithms 提案裡有基於正則表示式的新字串處理演算法。

RegexBuilder 文件

session Meet Swift RegexSwift Regex: Beyond the basics

Regex 示例程式碼如下: ```swift let s1 = "I am not a good painter" print(s1.ranges(of: /good/)) do { let regGood = try Regex("[a-z]ood") print(s1.replacing(regGood, with: "bad")) } catch { print(error) } print(s1.trimmingPrefix(/i am /.ignoresCase()))

let reg1 = /(.+?) read (\d+) books./ let reg2 = /(?.+?) read (?\d+) books./ let s2 = "Jack read 3 books." do { if let r1 = try reg1.wholeMatch(in: s2) { print(r1.1) print(r1.2) } if let r2 = try reg2.wholeMatch(in: s2) { print("name:" + r2.name) print("books:" + r2.books) } } catch { print(error) } ```

使用 regex builders 的官方示例: ```swift // Text to parse: // CREDIT 03/02/2022 Payroll from employer $200.23 // CREDIT 03/03/2022 Suspect A $2,000,000.00 // DEBIT 03/03/2022 Ted's Pet Rock Sanctuary $2,000,000.00 // DEBIT 03/05/2022 Doug's Dugout Dogs $33.27

import RegexBuilder let fieldSeparator = /\s{2,}|\t/ let transactionMatcher = Regex { /CREDIT|DEBIT/ fieldSeparator One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) // 👈🏻 we define which data locale/timezone we want to use fieldSeparator OneOrMore { NegativeLookahead { fieldSeparator } // 👈🏻 we stop as soon as we see one field separator CharacterClass.any } fieldSeparator One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } ```

在正則表示式中捕獲資料,使用 Capture: ```swift let fieldSeparator = /\s{2,}|\t/ let transactionMatcher = Regex { Capture { /CREDIT|DEBIT/ } // 👈🏻 fieldSeparator

Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) } // 👈🏻 fieldSeparator

Capture { // 👈🏻 OneOrMore { NegativeLookahead { fieldSeparator } CharacterClass.any } } fieldSeparator Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } // 👈🏻 } // transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)> ```

泛型與協議

session Embrace Swift genericsDesign protocol interfaces in Swift

swift 5.6 和之前編寫泛型介面如下: ```swift func feed(_ animal: A) where A: Animal

// 👆🏻👇🏻 Equivalents

func feed(_ animal: A) ```

swift 5.7 可以這樣寫: swift func feed(_ animal: some Animal)

some 關鍵字可以用於引數和結構型別。some 會保證型別關係,而 any 會持有任意具體型別,刪除型別關係。

SE-0347 Type inference from default expressions 擴充套件 Swift 泛型引數型別的預設值能力。如下程式碼示例: ```swift func suffledArray(from options: T = 1...100) -> [T.Element] { Array(options.shuffled()) }

print(suffledArray()) print(suffledArray(from: ["one", "two", "three"])) ```

SE-0341 Opaque Parameter Declarations 使用 some 引數簡化泛型引數宣告。SE-0328 Structural opaque result types 擴大不透明結果返回型別可以使用的範圍。SE-0360 Opaque result types with limited availability 可用性有限的不透明結果型別,比如 if #available(macOS 13.0, *) {} 就可以根據系統不同版本返回不同型別,新版本出現新型別的 View 就可以和以前的 View 型別區別開。

SE-0309 Unlock existentials for all protocols 改進了 existentials 和 泛型的互動。這樣就可以更方便的檢查 Any 型別的兩個值是否相等

any 關鍵字充當的是型別擦除的助手,是通過告知編譯器你使用 existential 作為型別,此語法可相容以前系統。

SE-0346 Lightweight same-type requirements for primary associated types 引入一種新語法,用於符合泛型引數並通過相同型別要求約束關聯型別。SE-0358 Primary Associated Types in the Standard Library 引入主要關聯型別概念,並將其帶入了標準庫。這些關聯型別很像泛型,允許開發者將給定關聯型別的型別指定為通用約束。

SE-0353 Constrained Existential Types 基於 SE-0309 和 SE-0346 提案,在 existential 型別的上下文中重用輕量關聯型別的約束。

SE-0352 Implicitly Opened Existentials 允許 Swift 在很多情況下使用協議呼叫泛型函式。

Swift 論壇上一個對 any 和 some 關鍵字語法使用場景的討論,Do any and some help with “Protocol Oriented Testing” at all?

Swift Concurrency

session Eliminate data races using Swift ConcurrencyVisualize and optimize Swift concurrencyMeet Swift Async Algorithms

表示持續時間有了新的放來來表達,對應提案是 SE-0329 Clock, Instant, and Duration ,continuous clock 是在系統睡眠狀態還會增加時間,suspending clock 在系統睡眠狀態不會增加時間。Instants 表示一個確定的時間。Duration 表示兩個時間經歷了多久。

新增 SE-0338 Clarify the Execution of Non-Actor-Isolated Async Functions 通過收緊可傳送性檢查的規則來避免潛在的資料競爭。

SE-0343 Concurrency in Top-level Code 這個提案主要是更好地支援命令列工具的開發,可以直接將 concurrency 程式碼寫到 main.swift 檔案裡。

SE-0340 Unavailable From Async Attribute 提供 noasync 語法以允許我們將型別和函式標記為在非同步上下文不可用。

Task 是按順序執行的,是非同步的,在 await 時可以暫停任意次數。task 是自包含的,有自己的資源,可以獨立於任何其他 task 獨立執行。task 通過在 body 末尾返回一個值來傳遞物件,值型別沒問題,如果是引用型別有可能出現數據競爭。

通過 Sendable 協議 Swift 可以幫助告訴我們什麼時候 task 之間共享資料是安全的。Sendable 描述的型別可以跨隔離 domain,不會有資料競爭,Swift 編譯器會在構建時檢查資料競爭。task 的返回型別要符合 Sendable。

引用型別只能在很少的情況下符合 Sendable。比如 final class 只有不可變的儲存。對於自己內部同步的引用型別,比如鎖,可以用 @unchecked Sendable 。 ```swift class ConcurrentCache: @unchecked Sendable { var lock: NSLock var storage: [Key: Value]

// ... } ```

Actor 提供了一種隔離狀態的方法可以消除資料競爭。使用 task 來執行 actor 定義的程式碼。一次只能在一個 actor 上執行一個 task。actor 也是依賴 Sendable。actor 是引用型別,但隔離了他們所有屬性和程式碼來防止併發訪問。@MainActor 表示的是主執行緒,你要在應用中更新 UI 時來用它。 ```swift @MainActor func updateView() { … }

Task { @MainActor in // update UI here } ```

@MainActor 也可以用於類,類的屬性和方法只能在主 main actor 上訪問,除非標記為 nonisolated 。 ```swift @MainActor class ChickenValley: Sendable { var flock: [Chicken] var food: [Pineapple]

func advanceTime() { for chicken in flock { chicken.eat(from: &food) } } } ```

Distributed Actors

actor 具有分散式形式工作能力,也就是可以 RPC 通過網路讀取和寫入屬性或者呼叫方法。設計為保護在跨多個程序中的低級別資料競爭。Distributed actors 可以在兩個程序間建立通道,隔離它們狀態,並在它們之間非同步通訊。每個 distributed actors 在 actor 初始化時分配一個不可以手動建立的 id,在它所屬整個 distributed actor 系統中唯一標識所指 actor,這樣無論 distributed actors 在哪,都可以以相同的方式與之互動。

session Meet distributed actors in Swift 。這裡有個 distributed actors 的程式碼示例 TicTacFish: Implementing a game using distributed actors

SE-0336 Distributed Actor IsolationSE-0344 Distributed Actor Runtime 是兩個 Distributed Actors 的相關提案。

Apple 提供了一個參考的服務端 cluster actor 系統實現示例,cluster actor system implementation

Optional

SE-0345 if let shorthand for shadowing an existing optional variable 引入的新語法,用於 unwrapping optinal。 ```swift let s1: String? = "hey" let s2: String? = "u" if let s1 { print(s1) }

guard let s1, let s2 else { return } print(s1 + " " + s2) ```

型別推斷

SE-0326 提高了 Swift 對閉包使用引數和型別推斷的能力。如下程式碼:

swift let a = [1,2,3] let r = a.map { i in if i >= 2 { return "\(i) 大於等於2" } else { return "\(i) 小於2" } } print(r)

Result Builders

SE-0348 buildPartialBlock for result builders 簡化了實現複雜 result buiders 所需的過載。

Swift-DocC

現在支援 Swift、OC 和 C,文件標記一樣。.doccarchive 包含可部署的網站內容,相容大多數託管服務,比如 Github pages。部署到線上服務上可參考 Generating Documentation for Hosting OnlinePublishing to GitHub Pages 文件。

和 SPM 整合參看 SwiftDocCPlugin

session 有 What’s new in Swift -DocCImprove the discoverability of your Swift-DocC content

SE-0356 Swift Snippets 程式碼片段用於示例文件的提案。

除錯

session Debug Swift debugging with LLDB

編譯器編譯 swift 檔案生成 .o 檔案會有 __debug_info 段,其中有可以對映到原始檔和行號的地址。debug 資訊可以連結到 .dSYM 包。debug 資訊連結器叫 dsymutil,dsymutil 可以為每個動態庫、framework 或 dylib 和可執行檔案打包一個 debug 資訊存檔(.dSYM 包)。

image 和路徑怎麼重對映。使用 image list nameOfFramework 來檢查 LLDB 是否找到了我們應用程式裡嵌入的第三方框架的 debug dSYM。使用 image lookup 0xMemoryAddressHere 獲取當前地址更多資訊。要重新對映原始檔 .dSYM 路徑,使用 settings set target.source-map old/path new/path。每個 .dSYM 都有一個 UUID.plist,我們可以在其中設定 DBGSourcePathRemapping 這個字典。

Xcode 14 新增 swift-healthcheck 命令,這個命令可以瞭解 module 為何匯入失敗。

LLDB 怎麼找到 Swift module?每個 .dSYM 包都可以包含二級制 swift module,其中可能包含橋標頭檔案、swift 介面檔案 .swiftinterface,還有 debug 資訊。靜態存檔不是由連結器生成的,需要向連結器註冊 swift module,使用 ld ... -add-ast-path /path/to/My.swiftmodule ,動態庫和可執行檔案的話,Xcode 會自動完成此操作。可以使用 dsymutil 來 dump 你可執行檔案的符號表,並用 grep 找 swiftmodule,命令是 dsymutil -s MyApp | grep .swiftmodule

記憶體管理

相關提案包括 SE-0349 Unaligned Loads and Stores from Raw MemorySE-0334 Pointer API Usability ImprovementsSE-0333 Expand usability of withMemoryRebound

Set 使用新的 Temporary Buffers 功能,讓 intersect 速度提升了 4 到 6 倍。

SwiftUI

介紹

Kuba Suder 做了一個 SwiftUI Index/Changelog ,從官方文件中提取版本資訊,一目瞭然 SwiftUI 每個版本 view,modifier 還有屬性做了哪些增加和改變。當然也包括這次 SwiftUI 4 的更新。還有份對今年更新整理的 cheat sheet What’s New In SwiftUI for iOS Cheat Sheet - WWDC22

SwiftUI 4 做了大量細節更新,比如添加了後臺任務函式 backgroundTask(_:action:) 。List 改用 UICollectionView。AnyLayout 讓 HStack 和 VStack 之間可以自由切換。scrollDismissesKeyboard() modifier 可以讓鍵盤在滾動時自動 dismiss。scrollIndicators() modifier 可以隱藏 ScrollView 和 List 等檢視的滾動指示。defersSystemGestures() modifier 允許我們的手勢優先於系統的內建手勢。顏色的 .gradient 可以獲得很簡單的漸變,Rectangle().fill(.red.gradient),還有 .shadow 用來建立投影 Rectangle().fill(.red.shadow(.drop(color: .black, radius: 10))),還有 .inner 內陰影。lineLimit() modifier 支援範圍設定。還有一些 modifier 支援 toggle 引數,比如 .bold().italic() 等,這樣利於執行時進行調整。

參考

session: - What’s new in SwiftUI

社群整理的和 SwiftUI 的 digital lounges 內容: - WWDC swiftui-lounge - WWDC 2022: Lessons from the SwiftUI Digital Lounges javier 整理的,做了詳細的分類 - #swiftui-lounge #wwdc22

Navigation 介面

控制導航啟動狀態、管理 size class 之間的 transition 和響應 deep link。

Navigation bar 有新的預設行為,如果沒有提供標題,導航欄預設為 inline title 顯示模式。使用 navigationBarTitleDisplayMode(_:) 改變顯示模式。如果 navigation bar 沒有標題、工具欄項或搜尋內容,它就會自動隱藏。使用 .toolbar(.visible) modifier 顯示一個空 navigation bar。

參考: - Migrating to New Navigation Types 官方遷移指南 - NavigationStack - NavigationSplitView - The SwiftUI cookbook for navigation

NavigationStack 的示例: swift struct PNavigationStack: View { @State private var a = [1, 3, 9] // 深層連結 var body: some View { NavigationStack(path: $a) { List(1..<10) { i in NavigationLink(value: i) { Label("第 \(i) 行", systemImage: "\(i).circle") } } .navigationDestination(for: Int.self) { i in Text("第 \(i) 行內容") } .navigationTitle("NavigationStack Demo") } } }

這裡的 path 設定了 stack 的深度路徑。

NavigationSplitView 兩欄的例子: ```swift struct PNavigationSplitViewTwoColumn: View { @State private var a = ["one", "two", "three"] @State private var choice: String?

var body: some View {
    NavigationSplitView {
        List(a, id: \.self, selection: $choice, rowContent: Text.init)
    } detail: {
        Text(choice ?? "選一個")
    }
}

} ```

NavigationSplitView 三欄的例子: ```swift struct PNavigationSplitViewThreeColumn: View { struct Group: Identifiable, Hashable { let id = UUID() var title: String var subs: [String] }

@State private var gps = [
    Group(title: "One", subs: ["o1", "o2", "o3"]),
    Group(title: "Two", subs: ["t1", "t2", "t3"])
]

@State private var choiceGroup: Group?
@State private var choiceSub: String?

@State private var cv = NavigationSplitViewVisibility.automatic

var body: some View {
    NavigationSplitView(columnVisibility: $cv) {
        List(gps, selection: $choiceGroup) { g in
            Text(g.title).tag(g)
        }
        .navigationSplitViewColumnWidth(250)
    } content: {
        List(choiceGroup?.subs ?? [], id: \.self, selection: $choiceSub) { s in
            Text(s)
        }
    } detail: {
        Text(choiceSub ?? "選一個")
        Button("點選") {
            cv = .all
        }
    }
    .navigationSplitViewStyle(.prominentDetail)
}

} ```

navigationSplitViewColumnWidth() 是用來自定義寬的,navigationSplitViewStyle 設定為 .prominentDetail 是讓 detail 的檢視儘量保持其大小。

SwiftUI 新加了個功能可以配置是否隱藏 Tabbar,這樣在從主頁進入下一級時就可以選擇不顯示底部標籤欄了,示例程式碼如下: swift ContentView().toolbar(.hidden, in: .tabBar)

相比較以前 NavigationView 增強的是 destination 可以根據值的不同型別展示不同的目的頁面,示例程式碼如下: swift struct PNavigationStackDestination: View { var body: some View { NavigationStack { List { NavigationLink(value: "字串") { Text("字串") } NavigationLink(value: Color.red) { Text("紅色") } } .navigationTitle("不同型別 Destination") .navigationDestination(for: Color.self) { c in c.clipShape(Circle()) } .navigationDestination(for: String.self) { s in Text("\(s) 的 detail") } } } }

Swift Charts

視覺化資料,使用 SwiftUI 語法來建立。還可以使用 ChartRenderer 介面將圖示渲染成圖。

官方文件 Swift Charts

入門參看 Hello Swift Charts

Apple 文章 Creating a chart using Swift Charts

高階定製和建立更精細圖表,可以看這個 session Swift Charts: Raise the bar 這個 session 也會提到如何在圖表中進行互動。這裡是 session 對應的程式碼示例 Visualizing your app’s data

圖表設計的 session,Design an effective chartDesign app experiences with charts

下面是一個簡單的程式碼示例: ```swift import Charts

struct PChartModel: Hashable { var day: String var amount: Int = .random(in: 1..<100) }

extension PChartModel { static var data: [PChartModel] { let calendar = Calendar(identifier: .gregorian) let days = calendar.shortWeekdaySymbols return days.map { day in PChartModel(day: day) } } }

struct PlayCharts: View { var body: some View { Chart(PChartModel.data, id: .self) { v in BarMark(x: .value("天", v.day), y: .value("數量", v.amount))

    }
    .padding()
}

}

struct PSwiftCharts: View { struct CData: Identifiable { let id = UUID() let i: Int let v: Double }

@State private var a: [CData] = [
    .init(i: 0, v: 2),
    .init(i: 1, v: 20),
    .init(i: 2, v: 3),
    .init(i: 3, v: 30),
    .init(i: 4, v: 8),
    .init(i: 5, v: 80)
]

var body: some View {
    Chart(a) { i in
        LineMark(x: .value("Index", i.i), y: .value("Value", i.v))
        BarMark(x: .value("Index", i.i), yStart: .value("開始", 0), yEnd: .value("結束", i.v))
            .foregroundStyle(by: .value("Value", i.v))
    } // end Chart
} // end body

} ```

BarMark 用於建立條形圖,LineMark 用於建立折線圖。SwiftUI Charts 框架還提供 PointMark、AxisMarks、AreaMark、RectangularMark 和 RuleMark 用於建立不同型別的圖表。註釋使用 .annotation modifier,修改顏色可以使用 .foregroundStyle modifier。.lineStyle modifier 可以修改線寬。

AxisMarks 的示例如下: swift struct MonthlySalesChart: View { var body: some View { Chart(data, id: \.month) { BarMark( x: .value("Month", $0.month, unit: .month), y: .value("Sales", $0.sales) ) } .chartXAxis { AxisMarks(values: .stride(by: .month)) { value in if value.as(Date.self)!.isFirstMonthOfQuarter { AxisGridLine().foregroundStyle(.black) AxisTick().foregroundStyle(.black) AxisValueLabel( format: .dateTime.month(.narrow) ) } else { AxisGridLine() } } } } }

可互動圖表示例如下: ```swift struct InteractiveBrushingChart: View { @State var range: (Date, Date)? = nil

var body: some View {
    Chart {
        ForEach(data, id: \.day) {
            LineMark(
                x: .value("Month", $0.day, unit: .day),
                y: .value("Sales", $0.sales)
            )
            .interpolationMethod(.catmullRom)
            .symbol(Circle().strokeBorder(lineWidth: 2))
        }
        if let (start, end) = range {
            RectangleMark(
                xStart: .value("Selection Start", start),
                xEnd: .value("Selection End", end)
            )
            .foregroundStyle(.gray.opacity(0.2))
        }
    }
    .chartOverlay { proxy in
        GeometryReader { nthGeoItem in
            Rectangle().fill(.clear).contentShape(Rectangle())
                .gesture(DragGesture()
                    .onChanged { value in
                        // Find the x-coordinates in the chart’s plot area.
                        let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x
                        let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x
                        // Find the date values at the x-coordinates.
                        if let dateStart: Date = proxy.value(atX: xStart),
                           let dateCurrent: Date = proxy.value(atX: xCurrent) {
                            range = (dateStart, dateCurrent)
                        }
                    }
                    .onEnded { _ in range = nil } // Clear the state on gesture end.
                )
        }
    }
}

} ```

社群做的更多 Swift Charts 範例 Swift Charts Examples

Advanced layout control

session Compose custom layouts with SwiftUI

提供了新的 Grid 檢視來同時滿足 VStack 和 HStack。還有一個更低級別 Layout 介面,可以完全控制構建應用所需的佈局。另外還有 ViewThatFits 可以自動選擇填充可用空間的方式。

Grid 示例程式碼如下: swift Grid { GridRow { Text("One") Text("One") Text("One") } GridRow { Text("Two") Text("Two") } Divider() GridRow { Text("Three") Text("Three") .gridCellColumns(2) } }

gridCellColumns() modifier 可以讓一個單元格跨多列。

ViewThatFits 的新檢視,允許根據適合的大小放檢視。ViewThatFits 會自動選擇對於當前螢幕大小合適的子檢視進行顯示。Ryan Lintott 的示例效果 ,對應示例程式碼 LayoutThatFits.swift

新的 Layout 協議可以觀看 Swift Talk 第 308 期 The Layout Protocol

通過符合 Layout 協議,我們可以自定義一個自定義的佈局容器,直接參與 SwiftUI 的佈局過程。新的 ProposedViewSize 結構,它是容器檢視提供的大小。 Layout.Subviews 是佈局檢視的子檢視代理集合,我們可以在其中為每個子檢視請求各種佈局屬性。 ```swift public protocol Layout: Animatable { static var layoutProperties: LayoutProperties { get } associatedtype Cache = Void typealias Subviews = LayoutSubviews

func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)

func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing

/// We return our view size here, use the passed parameters for computing the /// layout. func sizeThatFits( proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache // 👈🏻 use this for calculated data shared among Layout methods ) -> CGSize

/// Use this to tell your subviews where to appear. func placeSubviews( in bounds: CGRect, // 👈🏻 region where we need to place our subviews into, origin might not be .zero proposal: ProposedViewSize, subviews: Self.Subviews, cache: inout Self.Cache )

// ... there are more a couple more optional methods } ```

下面例子是一個自定義的水平 stack 檢視,為其所有子檢視提供其最大子檢視的寬度: ```swift struct MyEqualWidthHStack: Layout { /// Returns a size that the layout container needs to arrange its subviews. /// - Tag: sizeThatFitsHorizontal func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { guard !subviews.isEmpty else { return .zero }

let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let totalSpacing = spacing.reduce(0) { $0 + $1 }

return CGSize(
  width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
  height: maxSize.height)

}

/// Places the stack's subviews. /// - Tag: placeSubviewsHorizontal func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { guard !subviews.isEmpty else { return }

let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)

let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)
var nextX = bounds.minX + maxSize.width / 2

for index in subviews.indices {
  subviews[index].place(
    at: CGPoint(x: nextX, y: bounds.midY),
    anchor: .center,
    proposal: placementProposal)
  nextX += maxSize.width + spacing[index]
}

}

/// Finds the largest ideal size of the subviews. private func maxSize(subviews: Subviews) -> CGSize { let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in CGSize( width: max(currentMax.width, subviewSize.width), height: max(currentMax.height, subviewSize.height)) }

return maxSize

}

/// Gets an array of preferred spacing sizes between subviews in the /// horizontal dimension. private func spacing(subviews: Subviews) -> [CGFloat] { subviews.indices.map { index in guard index < subviews.count - 1 else { return 0 } return subviews[index].spacing.distance( to: subviews[index + 1].spacing, along: .horizontal) } } } ```

自定義 layout 只能訪問子檢視代理 Layout.Subviews ,而不是檢視或資料模型。我們可以通過 LayoutValueKey 在每個子檢視上儲存自定義值,通過 layoutValue(key:value:) modifier 設定。 ```swift private struct Rank: LayoutValueKey { static let defaultValue: Int = 1 }

extension View { func rank(_ value: Int) -> some View { // 👈🏻 convenience method layoutValue(key: Rank.self, value: value) // 👈🏻 the new modifier } } ```

然後,我們就可以通過 Layout 方法中的 Layout.Subviews 代理讀取自定義 LayoutValueKey 值: ```swift func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { let ranks = subviews.map { subview in subview[Rank.self] // 👈🏻 }

// ... } ```

要在佈局之間變化使用動畫,需要用 AnyLayout,程式碼示例如下: swift struct PAnyLayout: View { @State private var isVertical = false var body: some View { let layout = isVertical ? AnyLayout(VStack()) : AnyLayout(HStack()) layout { Image(systemName: "star").foregroundColor(.yellow) Text("Starming.com") Text("戴銘") } Button("Click") { withAnimation { isVertical.toggle() } } // end button } // end body }

同時 Text 和圖片也支援了樣式佈局變化,程式碼示例如下: ```swift struct PTextTransitionsView: View { @State private var expandMessage = true private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2))) private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))

var body: some View {
    Text("Dai Ming Swift Pamphlet")
        .font(expandMessage ? .largeTitle.weight(.heavy) : .body)
        .foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)
        .onTapGesture { withAnimation { expandMessage.toggle() }}
        .frame(maxWidth: expandMessage ? 150 : 250)
        .drawingGroup()
        .padding(20)
        .background(.cyan.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))
}

} ```

分享介面

Transferable 協議使資料可以用於剪下板、拖放和 Share Sheet。

可以在自己應用程式之間或你的應用和其他應用之間傳送或接受可傳輸專案。

支援 SwiftUI 來使用。

官方文件 Core Transferable

session Meet Transferable

新增一個專門用來接受 Transferable 的按鈕檢視 PasteButton,使用示例如下: swift struct PPasteButton: View { @State private var s = "戴銘" var body: some View { TextField("輸入", text: $s) .textFieldStyle(.roundedBorder) PasteButton(payloadType: String.self) { str in guard let first = str.first else { return } s = first } } }

ShareLink

ShareLink 檢視可以讓你輕鬆共享資料。示例程式碼如下: swift struct PShareLink: View { let url = URL(string: "https://ming1016.github.io/")! var body: some View { ShareLink(item: url, message: Text("戴銘的部落格")) ShareLink("戴銘的部落格", item: url) ShareLink(item: url) { Label("戴銘的部落格", systemImage: "swift") } } }

鎖屏的 Widget

和 WatchOS 一樣,可以瞟一眼就獲取資訊。

官方指南 Creating Lock Screen Widgets and Watch Complications

Bottom Sheet

SwiftUI 新推出的 presentationDetents() modifier 可以建立一個可以定製的 bottom sheet。示例程式碼如下: swift struct PSheet: View { @State private var isShow = false var body: some View { Button("顯示 Sheet") { isShow.toggle() } .sheet(isPresented: $isShow) { Text("這裡是 Sheet 的內容") .presentationDetents([.medium, .large]) } } }

detent 預設值是 .large。也可以提供一個百分比,比如 .presentationDetents([.fraction(0.7)]),或者直接指定高度 .presentationDetents([.height(100)])

presentationDragIndicator modifier 可以用來顯示隱藏拖動標識。

List

list 支援 Section footer。

list 分隔符可以自定義,使用 HorizontalEdge.leadingHorizontalEdge.trailing

list 不使用 UITableView 了。

今年 list 還新增了一個 EditOperation 可以自動生成移動和刪除,新增了 edits 引數,傳入 [.delete, .move] 陣列即可。這也是一個演示如何更好擴充套件和配置功能的方式。

ScrollView

新增 modifier swift ScrollView { ForEach(0..<300) { i in Text("\(i)") .id(i) } } .scrollDisabled(false) .scrollDismissesKeyboard(.interactively) .scrollIndicators(.visible)

TextField

支援多行,使用 Axis.vertical 以允許多行。TextField 超過行限制可以變成滾動檢視。

今年 TextField 可以嵌到 .alert 裡了。

Search

.searchable 支援 token 和 scope,示例如下: ```swift struct PSearchTokensAndScopes: View { enum AttendanceScope { case inPerson, online } @State private var queryText: String @State private var queryTokens: [InvitationToken] @State private var scope: AttendanceScope

var body: some View {
    invitationCountView()
        .searchable(text: $queryText, tokens: $queryTokens, scope: $scope) { token in
            Label(token.diplayName, systemImage: token.systemImage)
        } scopes: {
            Text("In Person").tag(AttendanceScope.inPerson)
            Text("Online").tag(AttendanceScope.online)
        }
}

} ```

Gauge

SwiftUI 引入一個新顯示進度的檢視 Gauge。

簡單示例如下: ```swift struct PGauge: View { @State private var progress = 0.45 var body: some View { Gauge(value: progress) { Text("進度") } currentValueLabel: { Text(progress.formatted(.percent)) } minimumValueLabel: { Text(0.formatted(.percent)) } maximumValueLabel: { Text(100.formatted(.percent)) }

    Gauge(value: progress) {

    } currentValueLabel: {
        Text(progress.formatted(.percent))
            .font(.footnote)
    }
    .gaugeStyle(.accessoryCircularCapacity)
    .tint(.cyan)
}

} ```

Group Form

Form 今年也得到了增強,示例如下: ```swift Form { Section { LabeledContent("Location") { AddressView(location) } DatePicker("Date", selection: $date) TextField("Description", text: $eventDescription, axis: .vertical) .lineLimit(3, reservesSpace: true) }

Section("Vibe") {
    Picker("Accent color", selection: $accent) {
        ForEach(Theme.allCases) { accent in
            Text(accent.rawValue.capitalized).tag(accent)
        }
    }
    Picker("Color scheme", selection: $scheme) {
        Text("Light").tag(ColorScheme.light)
        Text("Dark").tag(ColorScheme.dark)
    }

if os(macOS)

    .pickerStyle(.inline)

endif

    Toggle(isOn: $extraGuests) {
        Text("Allow extra guests")
        Text("The more the merrier!")
    }
    if extraGuests {
        Stepper("Guests limit", value: $spacesCount, format: .number)
    }
}

Section("Decorations") {
    Section {
        List(selection: $selectedDecorations) {
            DisclosureGroup {
                HStack {
                    Toggle("Balloons 🎈", isOn: $includeBalloons)
                    Spacer()
                    decorationThemes[.balloon].map { $0.swatch }
                }
                .tag(Decoration.balloon)

                HStack {
                    Toggle("Confetti 🎊", isOn: $includeConfetti)
                    Spacer()
                    decorationThemes[.confetti].map { $0.swatch }
                }
                .tag(Decoration.confetti)

                HStack {
                    Toggle("Inflatables 🪅", isOn: $includeInflatables)
                    Spacer()
                    decorationThemes[.inflatables].map { $0.swatch }
                }
                .tag(Decoration.inflatables)

                HStack {
                    Toggle("Party Horns 🥳", isOn: $includeBlowers)
                    Spacer()
                    decorationThemes[.noisemakers].map { $0.swatch }
                }
                .tag(Decoration.noisemakers)
            } label: {
                Toggle("All Decorations", isOn: [
                    $includeBalloons, $includeConfetti,
                    $includeInflatables, $includeBlowers
                ])
                .tag(Decoration.all)
            }

if os(macOS)

            .toggleStyle(.checkbox)

endif

        }

        Picker("Decoration theme", selection: themes) {
            Text("Blue").tag(Theme.blue)
            Text("Black").tag(Theme.black)
            Text("Gold").tag(Theme.gold)
            Text("White").tag(Theme.white)
        }

if os(macOS)

        .pickerStyle(.radioGroup)

endif

    }
}

} .formStyle(.grouped) ```

Button

.buttonStyle 可組合,示例如下: swift struct PButtonStyleComposition: View { @State private var isT = false var body: some View { Section("標籤") { VStack(alignment: .leading) { HStack { Toggle("Swift", isOn: $isT) Toggle("SwiftUI", isOn: $isT) } HStack { Toggle("Swift Chart", isOn: $isT) Toggle("Navigation API", isOn: $isT) } } .toggleStyle(.button) .buttonStyle(.bordered) } } }

Tap Location

可以獲取點選的位置,示例程式碼如下: swift Rectangle() .fill(.green) .frame(width: 50, height: 50) .onTapGesture(coordinateSpace: .global) { location in print("Tap in \(location)") }

其中 coordinateSpace 指定為 .global 表示位置是相對螢幕左上角,預設是相對當前檢視的左上角的位置。

選擇多個日期

MultiDatePicker 檢視會顯示一個日曆,使用者可以選擇多個日期,可以設定選擇範圍。示例如下: swift struct PMultiDatePicker: View { @Environment(\.calendar) var cal @State var dates: Set<DateComponents> = [] var body: some View { MultiDatePicker("選擇個日子", selection: $dates, in: Date.now...) Text(s) } var s: String { dates.compactMap { c in cal.date(from:c)?.formatted(date: .long, time: .omitted) } .formatted() } }

PhotosPick

支援圖片選擇,示例程式碼如下: ```swift import PhotosUI import CoreTransferable

struct ContentView: View { @ObservedObject var viewModel: FilterModel = .shared

var body: some View {
    NavigationStack {
        Gallery()
            .navigationTitle("Birthday Filter")
            .toolbar {
                PhotosPicker(
                    selection: $viewModel.imageSelection,
                    matching: .images
                ) {
                    Label("Pick a photo", systemImage: "plus.app")
                }
                Button {
                    viewModel.applyFilter()
                } label: {
                    Label("Apply Filter", systemImage: "camera.filters")
                }
            }
    }
}

} ```

Table

今年 iOS 和 iPadOS 也可以使用去年只能在 macOS 上使用的 Table了,據 digital lounges 裡說,iOS table 的效能和 list 差不多,table 預設為 plian list。我想 iOS 上加上 table 只是為了相容 macOS 程式碼吧。

table 使用示例如下: swift Table(attendeeStore.attendees) { TableColumn("Name") { attendee in AttendeeRow(attendee) } TableColumn("City", value: \.city) TableColumn("Status") { attendee in StatusRow(attendee) } } .contextMenu(forSelectionType: Attendee.ID.self) { selection in if selection.isEmpty { Button("New Invitation") { addInvitation() } } else if selection.count == 1 { Button("Mark as VIP") { markVIPs(selection) } } else { Button("Mark as VIPs") { markVIPs(selection) } } }

Toolbar

對 toolbar 的自定義,示例如下: swift .toolbar(id: "toolbar") { ToolbarItem(id: "new", placement: .secondaryAction) { Button(action: {}) { Label("New Invitation", systemImage: "envelope") } } } .toolbarRole(.editor)

SF Symbol

SF Symbol 支援變數值,可以通過設定 variableValue 來填充不同部分,比如 wifi 圖示,不同值會亮不同部分,Image(systemName: "wifi", variableValue: 0.5)

Gradient 和 Shadow

下面是個簡單示例: swift struct PGradientAndShadow: View { var body: some View { Image(systemName: "bird") .frame(width: 150, height: 150) .background(in: Rectangle()) .backgroundStyle(.cyan.gradient) .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0))) .font(.system(size: 60)) } }

Paul Hudson 使用 Core Motion 做了一個陰影隨裝置傾斜而變化的效果,非常棒,How to use inner shadows to simulate depth with SwiftUI and Core Motion

嵌入 UIKit

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

macOS

支援了 window,可以控制位置和大小。官方程式碼示例 Bringing multiple windows to your SwiftUI app

openWindow 程式碼示例如下: ```swift struct PartyPlanner: App { var body: some Scene { WindowGroup("Party Planner") { PartyPlannerHome() }

    Window("Party Budget", id: "budget") {
        Text("Budget View")
    }
    .keyboardShortcut("0")
    .defaultPosition(.topLeading)
    .defaultSize(width: 220, height: 250)
}

}

struct DetailView: View { @Environment(.openWindow) var openWindow

var body: some View {
    Text("Detail View")
        .toolbar {
            Button {
                openWindow(id: "budget")
            } label: {
                Image(systemName: "dollarsign")
            }
        }
}

} ```

session Bring multiple windows to your SwiftUI app 兩個新 Scene 型別。WindowGroup 允許多 window。MenuBarExtra。可程式設計方式開啟新 window 和 document。

MenuBarExtra 程式碼示例如下: ```swift struct PartyPlanner: App { var body: some Scene { Window("Party Budget", id: "budget") { Text("Budget View") }

    MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
        BulletinBoard()
    }
    .menuBarExtraStyle(.window)
}

} ```

講和 AppKit 混編的 session Use SwiftUI with AppKit

The craft of SwiftUI API design: Progressive disclosure 使用 windows 還有 MenuBarExtra,使用 modifier 來自定義應用程式 window 的 presentation 和行為。

使用 .dropDestination 來支援拖動。示例如下: swift .dropDestination(payloadType: Image.self) { receivedImages, location in guard let image = receivedImages.first else { return false } viewModel.imageState = .success(image) return true }

今年有新的 FormStyle ,示例如下: ```swift Form { Picker("Notify Me About:", selection: $notifyMeAbout) { Text("Direct Messages").tag(NotifyMeAboutType.directMessages) Text("Mentions").tag(NotifyMeAboutType.mentions) Text("Anything").tag(NotifyMeAboutType.anything) } Toggle("Play notification sounds", isOn: $playNotificationSounds) Toggle("Send read receipts", isOn: $sendReadReceipts)

Picker("Profile Image Size:", selection: $profileImageSize) {
    Text("Large").tag(ProfileImageSize.large)
    Text("Medium").tag(ProfileImageSize.medium)
    Text("Small").tag(ProfileImageSize.small)
}
.pickerStyle(.inline)

} .formStyle(.columns) ```

Apple 自身在 macOS 系統中使用了多少 SwiftUI 呢?郵件、iWork 和 Keychain Access 的部分檢視使用了,筆記、照片 和 Xcode 部分功能及新增功能的完整介面都是用的 SwiftUI,另外控制中心、字型冊和系統設定的大部分都是用 SwiftUI 開發了。

ImageRenderer

可以將 SwiftUI 的 View 生成圖片。

官方參考文件 ImageRenderer

後臺任務

session Efficiency awaits: Background tasks in SwiftUI 瞭解如何使用 SwiftUI 後臺任務 API 簡潔地處理任務。展示如何使用 Swift Concurrency 來處理網路響應、後臺重新整理等——同時保持效能和功率。

Xcode 14

Xcode 14 裡有新的 Swift 5.7,其中對泛型和協議有很大的改進。

參考

通用

編出來的二進位制小 30%。

改進了並行性,構建提速 25%。

改進了在 iOS 裝置上除錯 Swift 程式的效能。

提供單一圖示大小,Xcode 完成剩下的。

更智慧的程式碼完成,滾動時置頂類、結構體和函式名。錯誤訊息在重新處理時會變暗。

Xcode 搜尋和替換欄中可以使用正則表示式。相信以後社群會出現很多好用的正則表示式分享。

Xcode Organizer 中新增 Hang 報告,用來提供主執行緒上發生掛起的呼叫堆疊資訊,以及提供裝置和 iOS 版本資訊等統計資訊。

Xcode 14 現在支援為 iPadOS 開發 DriverKit 驅動程式。

建立新 C++ 專案,Clang 預設使用 C++20。已經實現了幾篇 C++20 和 C++2b 論文。

iOS、tvOS 和 watchOS 的構建預設不再包含 bitcode。

legacy 構建系統被刪除,LLVM 14 也不再支援 legacy。

Xcode 中的 Swift-DocC 現支援 OC 和 C 的 API 構建文件。生成的 Swift-DocC 文件網站包括一個新的導航側邊欄,用於瀏覽和過濾文件。可將 Swift-DocC 部署到 GitHub Pages。

效能問題修復

程式碼完成不再自動匯入模組。

提高了複雜表示式 SwiftUI 中程式碼完成的速度和準確性。

修復了包含大量錯誤或警告的檔案時導致效能下降的問題。

修復了 minimap 在長檔案時效能問題。

原始碼編輯器

滾動編輯器時,Xcode 會將程式碼結構的元素固定到編輯器頂部。

支援了 Regex 表示式語法高亮。Editor > Refactoring > Convert to Regex Builder 可以將正則文字轉成等效 Regex builder。

可以輸入匹配引數來選擇程式碼完成中預設引數的任意組合。

Swift 中程式碼完成提供基於變數名的 map、filter 和 contains 的 snippet。

提高 Swift 程式碼完成的準確性。

SwiftUI 的程式碼完成,現在有了 List 和 ForEach 的 snippet。

Xcode 14 還要很多貼心程式碼完成改進,比如寫 struct 的 init 可以自動完成。Codable 的 encode 也可以自動完成。

Xcode Preview

Preview 增強,預設是互動式的。

建立新專案會自動 resume。大量編輯時也不會暫停。會動態調整更新頻率。

Swift Packages

引入新引數 moduleAliases 來為衝突的模組定義唯一名稱,並以新名稱構建而不用改程式碼。注意的是起別名的模組要是純 Swift 模組。

允許使用 Swift Package command plugins。Xcode 為 Swift Package plugins 提供了 XcodeProjectPlugin 介面,這個介面擴充套件了 Swift Package Manager 的 PackagePlugin 介面。用這個介面可以獲得 Xcode 專案結構的簡化描述。

session 有 Meet Swift Package pluginsCreate Swift Package plugins

Instrument

Hang Tracing 工具,可以顯示應用程式的主執行緒什麼時候無法長時間處理傳入事件,從而導致 UI 卡住。

Runloop 工具,顯示 runloop 的使用和單獨的迭代,視覺上區分了程序中所有 runloop 的 runloop sleep 和 busy interval。

Instrument 新模板更方便除錯 distributed actors 和其它 Swift concurrency 特性。

memory graph 偵錯程式可以顯示 memory graph 的所有傳入和傳出引用。

Instrument 現有一個新的 Swift Concurrency 模板,用於跟蹤 swift concurrency 的使用。這個模板包括 Swift Tasks 工具,可顯示隨時間變化的 task 的狀態,總結 task 狀態,提供詳細的 task 描述,task 關係和 task 建立 callstacks 的呼叫樹結構。還有 Swift Actors 工具,可以跟蹤 actor 之間的 task 行為,顯示每個 actor 的 task 佇列,並幫助診斷 actor-isolated 程式碼等問題。

Instrument 裡的程式碼檢視更好顯示包含了效能資料。Interleave 模式,可以同時檢視原始碼和關聯的反彙編。原始碼檢視現在會在原始碼和反彙編判斷顯示 CPU 計數器,PMC 事件和動態公式。

修復了很多 Swift 相關顯示不友好的問題。

多端

官方例子 Configuring a multiplatform app 。一個示例了 NavigationSplitView、Layout、Chart 和 WeatherKit 的運用的官方例子 Food Truck: Building a SwiftUI multiplatform app

Session 筆記

https://www.wwdcnotes.com/notes/wwdc22/110371/

下面是 App Intents、WidgetKit 相關內容,這些都屬於 App Services,WWDC22 專門整理了 App Service 專題 。新系統服務比如 Messages collaboration、網路、CloudKit 的 System Service 主題

Widget

iOS 16 和 WatchOS 9 可以使用同一套程式碼編寫 widget。iOS 新增場景是鎖屏和 Live Activities(晚些時候推出)。

利用 Smart Stack,讓 widget 出現到棧頂,可以使用 TimelineEntryRelevance

官方參考: - WidgetKit 主題 - WidgetKit Session

介紹怎麼將 widgets 新增到 lock screen 的 session Complications and widgets: Reloaded 。對應的例項程式碼 Adding widgets to the Lock Screen and watch faces

App Intents

打通 App Shortcuts,從 Shortcuts 應用、Spotlight 和 Siri 執行你的 App 特定任務。

對應 Session - Dive into App Intents - Implement App Shortcuts with App Intents - Meet Focus filters - Design App Shortcuts

文件 App Intents

官方几篇 App Intents 文章: - Providing your app’s capabilities to system services - Integrating custom data types into your intents - App intents - Focus

對於 Shortcut 的使用少數派有篇很棒的文章 《iOS 快捷指令搭配 Notion API,更快速地編輯內容》 。

WeatherKit

Apple 收購 Dark Sky 後帶來了 WeatherKit 和 WeatherKit REST API。有著易用的 Swift 介面,還有配套的 REST API。WeatherKit 內建了 async/await 支援。

WeatherKit 指南 WeatherKit 文件

session Meet WeatherKit 。一個 Apple 提供的天氣程式碼示例 Fetching weather forecasts with WeatherKit

HealthKit

提供了更詳細的睡眠和鍛鍊資料。session 介紹 What’s new in HealthKit

Vision

更新介紹 session What’s new in Vision

VisionKit 現在有一個結合 AVCapture 和 Vision 的資料掃描器進行實時捕捉。 session Capture machine-readable codes and text with VisionKit

Live Text 介面

視覺庫的應用介面。可以從照片和暫停影片中獲取文字。

官方參考: - Add Live Text interaction to your app - Enabling Live Text interactions with images - Scanning data with the camera

ScreenCaptureKit

creenCaptureKit 框架可以給你的 macOS 程式新增對高效能螢幕錄製的支援。文件地址:ScreenCaptureKit

App Store

內購

可以將 App Store Connect 內購產品同步到 Xcode。

新測試功能,比如在沙盒和 Xcode 裡請求測試通知和測試其它應用內購買場景。

官方參考: - WWDC22 的 App Store 釋出和市場的專題 - StoreKit2 主題 - What's new in StoreKit testing - What’s new with in-app purchase - Implement proactive in-app purchase restore - Explore in-app purchase integration and migration - Discover Benchmarks in App Analytics - What’s new with SKAdNetwork - App Store Server Notifications V2 - App Store Server API - App Analytics - SKAdNetwork 主題 What's new with SKAdNetwork - 自動更新訂閱 - 管理自動續期訂閱的定價

這裡有個 Kevin 開源的微信支付 SDK wechatpay-swift

全球化

session Build global apps: Localization by example

request review

你可以用 requestReview 這個 environment 鍵提示使用者對你的 App 進行評論。示例程式碼如下: swift struct PRequestReview: View { @Environment(\.requestReview) var rr var body: some View { Button("來評論吧") { rr() } } }

Apple 的最佳實踐例子 Requesting App Store Reviews

參考

稽核

這次稽核,規則 4.2.3 中取消二進位制要有啟動時足夠的內容,這可能是因為 Background Assets 的推出可以讓使用者更快更聰明的下載。另外 5.3.3 放寬了彩排等限制。

效能

Apple 除了做編譯優化體積外,還提供了一個 Background Assets 在應用安裝後、應用更新時以及應用保留在裝置上時定期在後臺下載資源,看起來類似 ODR。Background Assets 的 session Meet Background Assets

官方參考: - Link fast: Improve build and launch times - MetricKit - MXMetricManager 管理自定義指標 - MXAppLaunchDiagnostic 啟動診斷 - MXAppLaunchMetric 啟動相關指標 - Improve app size and runtime performance - Track down hangs with Xcode and on-device detection - Power down: Improve battery consumption

硬體和虛機

官方參考: - Virtualization 文件 建立虛擬機器並執行基於 macOS 和 Linux 的作業系統 - DriverKit 文件 開發在使用者空間執行的裝置驅動程式 - SCSIPeripheralsDriverKit - DeviceDiscoveryExtension - Running GUI Linux in a virtual machine on a Mac (示例程式碼)

session 有: - Bring your driver to iPad with DriverKit - Create macOS or Linux virtual machines

網路

session Reduce networking delays for a more responsive appBuild device-to-device interactions with Network Framework

Metal 3

利用多核優勢,高解析度圖形渲染更快,資源載入更快。使用 GPU 訓練機器學習網路。WWDC22 期間社群有個給背景新增雨水效果有些流行,作者放出了程式碼,介紹瞭如何將 Metal 引入 SwiftUI 工作流,Atmos

官方參考: - Metal 主題 - Metal Session

RoomPlan

ARKit 支援的新 Swift 介面。使用攝像頭和 LiDAR 建立 3D 平面圖。另外還有一個視覺庫的程式碼例子很有趣,就是從影片中檢測人物行為,Detecting Human Actions in a Live Video Feed

官方參考: - RoomPlan 主題 - RoomPlan 文件

session Create parametric 3D room scans with RoomPlan 。官方示例程式碼 Create a 3D model of an interior room by guiding the user through an AR experience

Passkeys

身份驗證,使用行業標準。

官方參考: - Passkeys 主題 - Meet passkeys

互動設計

Apple 的人機介面互動指南 Human Interface Guidelines 。內容超級詳細,涉及程式介面方方面面。

官方參考: - Human Interface Guidelines > What’s New 內有圖示模板等,各類 Session 彙總

資料