利用 UIViewControllerRepresentable 協定 在 SwiftUI 存取相簿並使用相機


先前我們曾探討 UIViewRepresentable 的用法,並展示了如何整合 UITextView 到 SwiftUI 專案中。雖然我們可以使用 UIViewRepresentable 協定包裝 UIKit 視圖 (View),但是視圖控制器 (View Controller) 呢?你可能需要在 App 中使用相機或存取使用者的相簿。那麼,如何將 UIImagePickerController 類別整合到 SwiftUI 視圖中呢?

在本教程中,我們將使用 UIViewControllerRepresentable 協定帶你完成整合作業。它與 UIViewRepresentable 協定非常相似,但是 UIViewControllerRepresentable 是為包裝 UIKit 視圖控制器而設計的。如果你已經閱讀了 UIViewRepresentable教程,就應該對以下將要討論的步驟非常熟悉。

使用 UIViewControllerRepresentable

基本上,要將 UIImagePickerController 整合到 SwiftUI 專案中,我們可以使用 UIViewControllerRepresentable 協定,來包裝控制器並實現所需的方法:

struct ImagePicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {

        // Return an instance of UIImagePickerController
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }
}

這個協定有兩個需要遵從的方法。當初始化 ImagePicker 時,就會調用 makeUIViewController 方法。在該方法中,你需要實例化 (instantiate) UIImagePickerController,並配置其初始狀態。另外,在 App 狀態有所更改而影響 ImagePicker 時,就會調用 updateUIViewController 方法。你可以實作這個方法,來更新 UIImagePickerController 的配置。如果沒有要更新的內容,你也可以將該方法留空。

在 SwiftUI 中創建 ImagePicker

與往常一樣,我喜歡通過構建範例專案來說明 API。讓我們使用 Single View Application 模板 (template),創建一個名為 SwiftUIImagePicker 的新專案。請在 User Interface 選項中選擇 SwiftUI

接下來,我們將為 ImagePicker 創建一個新檔案。右鍵單擊專案 navigator 中的 SwiftUIImagePacker 文件夾,然後選擇 New File…,再選擇 Swift File 模板。 首先,我們需要匯入 UIKit 和 SwiftUI 框架:

import UIKit
import SwiftUI

接下來,創建 ImagePicker 結構 (struct),並採用 UIViewControllerRepresentable 協定:

struct ImagePicker: UIViewControllerRepresentable {

    var sourceType: UIImagePickerController.SourceType = .photoLibrary

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {

        let imagePicker = UIImagePickerController()
        imagePicker.allowsEditing = false
        imagePicker.sourceType = sourceType

        return imagePicker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }
}

UIImagePickerController 類別允許你訪問相簿,並使用內建相機。在上面的程式碼中,我們為此宣告一個 sourceType 變數 (variable)。在預設情況下,它會設置為打開使用者的相簿。在 makeUIViewController 方法中,我們實例化 UIImagePickerController 的實例,並配置它的 source type。

使用 ImagePicker 在 SwiftUI 視圖中加載相簿

現在我們已經創建了 ImagePicker,下一步來看看如何在 SwiftUI 視圖中使用它吧。切換到 ContentView.swift,並如此更新 ContentView 結構:

struct ContentView: View {

    @State private var isShowPhotoLibrary = false
    @State private var image = UIImage()

    var body: some View {
        VStack {

            Image(uiImage: self.image)
                .resizable()
                .scaledToFill()
                .frame(minWidth: 0, maxWidth: .infinity)
                .edgesIgnoringSafeArea(.all)

            Button(action: {
                self.isShowPhotoLibrary = true
            }) {
                HStack {
                    Image(systemName: "photo")
                        .font(.system(size: 20))

                    Text("Photo library")
                        .font(.headline)
                }
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 50)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(20)
                .padding(.horizontal)
            }
        }
    }
}

我們有一個狀態變數用來控制相簿的出現時機,而另一個狀態變數用於所選圖像,在預設情況下,所選圖像設置為空白。在視圖中,我們有一個可以更改 isShowPhotoLibrary 狀態的按鈕。如果你正確地編寫了程式碼,應該會在預覽中看到以下內容:

swiftui-image-picker-photo-library-camera

要使用 ImagePicker 加載相簿,請將 .sheet 修飾符 (modifier) 加到 VStack 中:

.sheet(isPresented: $isShowPhotoLibrary) {
    ImagePicker(sourceType: .photoLibrary)
}

在閉包中,我們創建了 ImagePicker,並在使用者點擊 Photo Library 按鈕時顯示相簿,在預覽畫布 (preview canvas) 中運行 App,應該可以打開相簿。

swiftui-uiimagepickercontroller-photo-library

讓 Coordinator 採用 UIImagePickerControllerDelegate 協定

目前,內容視圖對所選圖片一無所知。你在相簿中選擇了一張照片後,App 只會關閉相簿視圖並返回主螢幕。

如果你有曾經用過 UIImagePickerController,就會知道必須採用兩個委託 (delegate):UIImagePickerControllerDelegateUINavigationControllerDelegate,以便與 UIImagePickerController 進行交互。當使用者從相簿中選擇照片時,將調用委託的 imagePickerController(_:didFinishPickingMediaWithInfo:) 方法。

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

}

實作該方法後,我們可以從方法的參數中獲取選定的照片。要在 ImagePicker 中採用該協定,我們需要實作 makeCoordinator 方法,並提供一個 Coordinator 實例,這個 Coordinator 充當控制器的委託與 SwiftUI 之間的一道橋樑。

現在,讓我們回到 ImagePicker.swift,並宣告兩個變數:

@Binding var selectedImage: UIImage
@Environment(\.presentationMode) private var presentationMode

顧名思義,selectedImage 變數用於儲存所選圖像。使用者選擇照片後,我們需要關閉相簿。為此,我們使用了 presentationMode 變數。稍後,我們可以調用 presentationMode.wrappedValue.dismiss() 來關閉視圖。

接下來,在 ImagePicker 內部創建 Coordinator 類別:

final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    var parent: ImagePicker

    init(_ parent: ImagePicker) {
        self.parent = parent
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

        if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            parent.selectedImage = image
        }

        parent.presentationMode.wrappedValue.dismiss()
    }
}

Coordinator 類別採用 UIImagePickerControllerDelegate 協定,並實作 imagePickerController(_:didFinishPickingMediaWithInfo:) 委託方法。當選擇好一個圖像時,就會調用該方法。在該方法中,我們取回選定的圖像,然後關閉相簿。

init 方法會接受 ImagePicker 的實例,以便我們將選定的圖像傳遞給它,並使用其 presentationMode 來關閉視圖。

現在我們已經準備好 Coordinator 類別了,下一步是創建 makeCoordinator 方法,並回傳 Coordinator 的實例:

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

最後,我們需要將 coordinator 分配給 UIImagePickerController 的委託。在 makeUIViewController 方法中,插入以下程式碼:

imagePicker.delegate = context.coordinator

在測試更改之前,讓我們切換到 ContentView.swift,並更新 .sheet 修飾符中的程式碼:

.sheet(isPresented: $isShowPhotoLibrary) {
    ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}

我們需要傳遞 image 的綁定,讓 ImagePicker 可以傳遞使用者選擇的圖像進行顯示。現在,在模擬器或預覽畫布中運行 App,現在應該可以顯示所選圖像。

swiftui-image-picker-load-photo

在 SwiftUI 中使用相機

ImagePicker 具有足夠的靈活性以支援內建相機。如果你想在 SwiftUI App 中打開相機拍照,可以將 sourceType.photoLibray 更改為 .camera,如下所示:

ImagePicker(sourceType: .camera, selectedImage: self.$image)

在 iOS 中,使用者必須明確授予每個 App 訪問相機的權限。所以,除了上述更改之外,我們還需要編輯 Info.plist 文件,並指定 App 需要使用內置相機的原因。

swiftui-camera-configuration

你無法使用模擬器測試這一點。但是,如果你有一台 iPhone,就可以將 App 配置到設備上進行測試。

編者提醒:如果你想了解有關構建全屏相機的技巧,可以參考這篇教程

總結

在這篇文章中,我們介紹了 UIViewControllerRepresentable 協定,它是 UIKit 視圖控制器和 SwiftUI 視圖之間的一道橋樑。我們利用這個協定,將 UIImagePickerController 整合到了 SwiftUI 專案中,從而允許 App 訪問相簿和設備的相機。

SwiftUI 仍然是一個非常新的框架,因此並非所有 UIKit 元件可用於都在這個框架中。如果你在 SwiftUI 中找不到原生 UI 元件,就可以應用這個技巧,將 UIKit 元件引入 SwiftUI 專案。

你可以在 GitHub 上下載完整專案作參考。

譯者簡介:陳奕先 - 過去為平面財經記者,專跑產業新聞,2015 年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校 ALPHA Camp 畢業後,積極投入 iOS 程式開發,目前任職於國內電商公司。
聯絡方式:
電郵: [email protected]
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文How to Access Photo Library and Use Camera in SwiftUI


軟件工程師,AppCoda 創辦人。著有《iOS 13 App 程式設計實力超進化實戰攻略》、《iOS 13 App 程式設計實力超進化實戰攻略》以及《精通 SwiftUI》。曾任職於HSBC, FedEx等公司,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda,致力於iOS程式教學,產品設計及開發。

blog comments powered by Disqus
Shares
Share This