[2024/12/24更新]
- 新版macOS 15的PhotosPicker預設行為與最初不同,必須加上 .photosPickerStyle(.inline),否則無法選擇照片,選擇後也不會自己消失。
- 測試環境:Mac mini 2023 (M2), macOS 15.2, Swift Playgrounds 4.5.1
在第4單元第9課學習圖片輪播時,後半段(4-9e)曾製作一個相簿App,當時開啟相簿的功能是透過UIKit 物件,也就是在SwiftUI中橋接UIKit的方式,相當麻煩。
Swift Playgrounds 4.2 之後,終於可以直接在SwiftUI中開啟相簿,這個物件名為 PhotosPicker (相片選擇器),與UIKit用的PHPicker 同樣放在 PhotosUI 框架中,需要 import PhotosUI 才能使用。
PhotosPicker 是一個視圖物件,因此用法跟其他視圖類似,非常簡單,但是背後過程卻不容易理解,因此我們從最簡化的範例開始(在電子書 .playgroundbook 模式下測試):
// 4-11a PhotosPicker開啟相簿
// Created by Heman, 2022/10/29
// Updated by Heman, 2024/12/24. (增加 .photosPickerStyle(.inline) 兩行)
import SwiftUI
import PhotosUI
struct 開啟相簿: View {
@State var 選擇: PhotosPickerItem?
var body: some View {
PhotosPicker(selection: $選擇, matching: .images) {
Text("請選擇照片(單選)")
.font(.title)
}
.photosPickerStyle(.inline)
.photosPickerAccessoryVisibility(.hidden, edges: .leading)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(開啟相簿())
PhotosPicker最少需要兩個參數(匿名函式也算參數):
(1) 括號(selection: $選擇)是一個要帶回選擇內容的參數(注意 $ 符號)
(2) 匿名函式 { } 則是提示開啟相簿的「標籤按鈕」,可放任何視圖,此例用 Text() 呈現
注意括號內參數「選擇」的類型,並不是我們期望的 Image 或 UIImage,而是一個新的類型:PhotosPickerItem。若執行上述範例的話,會出現相簿沒錯,但選擇圖片後並不會顯示圖片,而是重回到「請選擇照片(單選)」的標籤按鈕。如以下執行結果:
想知道傳回的 PhotosPickerItem 到底是什麼內容嗎?我們修改範例程式來偷看一下:
// 4-11b 查看PhotosPickerItem
// Created by Heman, 2022/10/29
// Updated by Heman, 2024/10/27. (增加 .photosPickerStyle(.inline) 兩行
import SwiftUI
import PhotosUI
struct 開啟相簿: View {
@State var 選擇: PhotosPickerItem?
var body: some View {
PhotosPicker(selection: $選擇) {
Text("請選擇照片(單選)")
.font(.title)
}
.photosPickerStyle(.inline)
.photosPickerAccessoryVisibility(.hidden, edges: .leading)
.onChange(of: 選擇) { newItem in
print(newItem)
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(開啟相簿())
4-11b 加了 .onChange() 修飾語,當參數「選擇」變更時,就會將內容 print 出來,執行結果列印到主控台的內容如下(有編排過):
Optional(_PhotosUI_SwiftUI.PhotosPickerItem(
_itemIdentifier: FDE85F22-6B6E-449B-90DF-EDD345B0580B/L0/001,
_shouldExposeItemIdentifier: false,
_supportedContentTypes: [<_UTCoreType 0x7ff846f59bc0> public.jpeg (not dynamic, declared)],
_itemProvider: <puphotosfileprovideritemprovider: 0x7fc7a97acd90=""> {types =
("com.apple.private.live-photo-bundle", "public.jpeg")}
))
可以看到 PhotosPickerItem 包含4個屬性,名稱都是以底線 _ 開頭,表示這是系統內部結構,僅供 Apple 使用,所以我們無需深究,只需知道這不是我們要的 Image 或 UIImage。
那麼要如何才能得到所選照片呢?根據原廠文件,必須用 PhotosPickerItem 的物件方法 loadTransferable() 才能取得照片資料,也就是說,上面列印出來的 PhotosPickerItem 4個屬性,其實只是取得照片的「線索」。
進一步改寫範例如下:
// 4-11c loadTransferable()
// Created by Heman, 2022/10/29
// Updated by Heman, 2024/10/27. (增加 .photosPickerStyle(.inline) 兩行
import SwiftUI
import PhotosUI
struct 開啟相簿: View {
@State var 選擇: PhotosPickerItem?
@State var 圖片: UIImage?
var body: some View {
PhotosPicker(selection: $選擇) {
Text("請選擇照片(單選)")
.font(.title)
}
.photosPickerStyle(.inline)
.photosPickerAccessoryVisibility(.hidden, edges: .leading)
.onChange(of: 選擇) { newItem in
print(newItem)
Task {
do {
if let 原始資料 = try await newItem?.loadTransferable(type: Data.self) {
if let 轉換圖片 = UIImage(data: 原始資料) {
圖片 = 轉換圖片
print(圖片)
}
}
} catch {
print("無法取得或轉換照片: \(error)")
}
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(開啟相簿())
loadTransferable() 是一個非同步 async throws 函式,須以 try await 呼叫,所以用 Task-do-try-await-catch 標準句型。等 loadTransferable() 取得資料後,還要用 UIImage 轉換為圖片格式,才能在 Image 視圖顯示出來。
這段用 try await 取得原始資料,再經由 UIImage 轉換的寫法,是否覺得似曾相識?沒錯,在第9課一開始4-9a也曾用過,當時呼叫的是URLSession.shared.data():
// 4-9a 片段
Task {
if let myURL = URL(string: 網址) {
do {
let (內容, 回應碼) = try await URLSession.shared.data(from: myURL)
if let 轉圖 = UIImage(data: 內容) {
...
}
} catch {
print("有錯誤發生")
}
}
}
到這裡我們可以推測,loadTransferable() 和 URLSession.shared.data() 功能類似,都是到某個地方抓圖。但是,相簿不就在我們自己電腦裡面嗎?
未必如此,因為 Apple 已將相簿與 iCloud 整合,也就是說,有些情況下,照片有可能在雲端,而不在本機。所以透過非同步的 loadTransferable(),不管照片在本機或在雲端,loadTransferable() 都會幫我們抓下來。
到此我們終於拿到從相簿選擇的照片,接下來就可以用 Image 顯示出來。再次修改範例,模擬一個常見的會員頭像設定,當還未選擇照片時,先放系統圖示"person.circle",選擇後再改成照片,兩者都以 .mask(Circle()) 剪裁成圓形。
注意圖片視圖 Image() 放在原來「標籤按鈕」Text() 的位置,所以圖片本身就是按鈕,按一下就可開啟相簿重新選擇。
完整程式碼如下:
// 4-11d 選擇頭像照片
// Created by Heman, 2022/10/29
// Updated by Heman, 2024/10/27. 全部改寫
import SwiftUI
import PhotosUI
struct 選擇頭像: View {
@State var 單選: PhotosPickerItem?
@State var 頭像照: UIImage? = nil
var body: some View {
if 頭像照 == nil {
PhotosPicker(selection: $單選) {
Image(systemName: "person.circle")
.resizable()
.scaledToFit()
.mask(Circle())
}
.photosPickerStyle(.inline)
.photosPickerAccessoryVisibility(.hidden, edges: .leading)
.onChange(of: 單選) { newItem in
Task {
do {
if let 原始資料 = try await newItem?.loadTransferable(type: Data.self) {
if let 轉換圖片 = UIImage(data: 原始資料) {
頭像照 = 轉換圖片
}
}
} catch {
print("無法取得或轉換照片: \(error)")
}
}
}
} else {
Image(uiImage: 頭像照!)
.resizable()
.scaledToFit()
.mask(Circle())
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(選擇頭像())
執行結果:
💡 註解
- Apple 為了做 PhotosPicker 視圖,設計了一個新的規範(protocol),稱為 Transferable (字面上是「可轉移的」意思),用來實現兩個App之間的資料傳遞。
- PhotosPicker 開啟的相簿,其實是另一個App(即作業系統內建的「照片」App)操作的,資料實際是從「照片」App傳遞過來,因此稱為 loadTransferable()。
- Transferable 規範除了傳遞照片之外,未來還可以讓SwiftUI實現「剪貼(Clipboard)」或「拖放(Drag-and-drop)」等功能。