在 iOS 16 推出之前,如果我們想要顯示一個 Photo Picker,讓使用者可以從相片圖庫 (Photo Library) 中選擇相片,就需要使用 UIKit 的PHPickerViewController
或更舊的 UIImagePickerController
。雖然實際使用並不困難,畢竟我們可以利用 UIViewControllerRepresentable
來整合 UIKit 組件,但是,如果 SwiftUI 框架有 Photo Picker 的原生視圖 (native view) 就更好了!
在 iOS 16,Apple 終於在 SwiftUI 加入 PhotosPicker,與 UIKit 的組件有相同的功能。如果你的 App 只支援運行 iOS 16 或以上版本的設備,就可以使用這個新視圖來處理選擇相片的操作。
在這篇文章中,我們會以一些範例程式碼來看看如何使用 PhotosPicker。請注意,你需要使用 Xcode 14 beta 4 版本,才可以跟著這篇教學進行實作。
在 SwiftUI 使用 PhotosPicker
PhotosPicker
視圖是在 PhotosUI
框架中,因此在使用之前,我們需要先匯入這個框架:
import PhotosUI
接著,讓我們宣告 (declare) 一個狀態變數 (state variable) 來保存所選相片:
@State private var selectedItem: PhotosPickerItem?
要調用 Photo Picker 非常簡單,以下就是 PhotosPicker
的基本用法:
PhotosPicker(selection: $selectedItem, matching: .images)) {
Label("Select a photo", systemImage: "photo")
}
.tint(.purple)
.controlSize(.large)
.buttonStyle(.borderedProminent)
我們把 binding 傳遞給所選相片和相片過濾器 (photo filter),來實例化 PhotosPicker
。在閉包中,我們可以描述按鈕的外觀。只需要幾行程式碼,Xcode 就會在預覽中顯示一個按鈕。
如果我們點擊按鈕,就會顯示一個 Photo Picker 來讓我們從相片圖庫選擇相片。當我們選擇了一張相片後,Photo Picker 就會自動關閉,而所選的相片就會被儲存在 selectedItem
變數中。
過濾相片
我們可以使用 matching
參數,來指定要應用到相片圖庫的相片過濾器。在上面的程式碼中,我們把 matching
數值設置為 .images
,也就是只顯示相片。如果我們想同時顯示相片和影片,可以如此設置參數的數值:
.any(of: [.images, .videos])
.images
過濾器會包含使用者相片圖庫內的所有相片。如果我們想從相片圖庫中排除原況相片 (live photo),就可以這樣設置參數的數值:
.any(of: [.images, .not(.livePhotos)])
我們使用了 .not
過濾器來排除原況相片。
處理相片選擇
如前文所述,所選的相片會被儲存在 selectedItem
變數中,這個變數的型別是 PhotoPickerItem
。那我們如何可以加載相片,並在螢幕上顯示相片呢?
首先,讓我們附加 onChange
修飾符,來監聽 (listen) selectedItem
變數的更新。每當變數有變化時,我們就調用 loadTransferable
方法來加載 asset data。
.onChange(of: selectedItem) { newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self) {
selectedPhotoData = data
}
}
}
在 WWDC22 What's new in the Photos picker 的演講中,Apple 的工程師展示了如何把型別指定為 Image.self
,就是指示 loadTransferable
回傳 Image
的實例。但是我無法在 Xcode 14 beta 4 上如此操作,因此我改用了 Data.self
。之後,我們就可以把 data 轉換為 UIImage
物件,並在 Image
視圖中顯示。
另一個用來保存 data 物件的狀態變數就是 selectedPhotoData
:
@State private var selectedPhotoData: Data?
接著,讓我們使用 image data 創建 UIImage
的實例,並把它傳遞給 Image
視圖:
if let selectedPhotoData,
let image = UIImage(data: selectedPhotoData) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.clipped()
}
以上就是我們處理相片選擇的方法。簡單來說,當使用者在相片圖庫選擇相片時,我們會存取 image data,並把 data 保存到狀態變數(即 selectedPhotoData
)中,SwiftUI 偵測到數值的變化時,就會觸發 UI 更新,以在螢幕上呈現相片。
選擇多張相片
PhotosPicker
視圖也支援選擇多張相片的功能,讓我們實作一個簡單的範例來看看吧!和之前一樣,我們有兩個狀態變數來保存 PhotosPickerItem
物件和 Data
物件。因為使用者可能選擇多於一張相片,所以這兩個變數都變成了陣列 (array):
@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedPhotosData: [Data] = []
要支援選擇多張相片的功能,我們要使用 PhotosPicker
另一個初始化方法:
PhotosPicker(selection: $selectedItems, maxSelectionCount: 5, matching: .images) {
Image(systemName: "photo.on.rectangle.angled")
}
.onChange(of: selectedItems) { newItems in
for newItem in newItems {
Task {
if let data = try? await newItem.loadTransferable(type: Data.self) {
selectedPhotosData.append(data)
}
}
}
}
這個方法有一個 maxSelection
參數。如果我們把數值設置為 5
,就代表使用者最多可以選擇 5 張相片。在這種情況下,我們可能會在 onChange
閉包中獲取多於一張相片。我們要做的就是加載每個相片項目,並添加到 data 陣列(selectedPhotosData
) 中。
在這個範例視圖中,按鈕不是在螢幕中央,而是在導航欄中。以下是完整的程式碼:
NavigationStack {
ScrollView {
VStack {
ForEach(selectedPhotosData, id: \.self) { photoData in
if let image = UIImage(data: photoData) {
Image(uiImage: image)
.resizable()
.scaledToFit()
.cornerRadius(10.0)
.padding(.horizontal)
}
}
}
}
.navigationTitle("Photos")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
PhotosPicker(selection: $selectedItems, maxSelectionCount: 5, matching: .images) {
Image(systemName: "photo.on.rectangle.angled")
}
.onChange(of: selectedItems) { newItems in
for newItem in newItems {
Task {
if let data = try? await newItem.loadTransferable(type: Data.self) {
selectedPhotosData.append(data)
}
}
}
}
}
}
}
當 selectedPhotosData
變數有任何變化時,SwiftUI 就會更新 UI 並在滾動視圖 (scroll view) 中顯示相片。
如果你喜歡這篇文章,又有興趣深入學習 SwiftUI,歡迎查閱我們的《精通 SwiftUI》一書。