Last Updated:
Photo by Harpal Singh on Unsplash

Integrating UIKit with SwiftUI, 初探

由於 SwiftUI 是從 iOS 13.0 才開始導入(目前版本為 iOS 14.4),相較於遠從 iOS 2.0 就已經存在的 UIKit 來說,簡直就像是新生兒一般的存在。因此,某些時候我們會希望能直接利用 UIKit 或其他較成熟的 Apple framework 所提供的程式碼來進行開發。

在進入正題之前,需對 UIKit 有一些基本的認識:

  • UIKit 的UIView class 是 layout 底下所有 views 的 parent class(views 包含 sliders、buttons…)。
  • 而 UIKit 的UIViewController class 則是負責將 view 實現的程式碼,它和 UIView 一樣都擁有許多 subclass 負責不同的任務。
  • UIKit 使用的 design pattern 稱為 delegation(委派),當 view 的狀態改變時就由它接手處理。

接下來,將以「使用者從 photo library(照片圖庫)挑選圖片」來進行說明與示範。這個動作將會使用 UIKit 底下的UIImagePickerController,和兩個 delegate protocol - 分別是UINavigationControllerDelegateUIImagePickerControllerDelegate,但 SwiftUI 無法直接使用它們。

首先,UIImagePickerControllerUIViewController的一個 subclass,所以將它包裹(wrap)在 struct 底下時,struct 必須要符合UIViewControllerRepresentable protocol(此處將 custom struct 取名為 “ImagePicker”)。

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    // more code
}

根據如下的 SwiftUI 定義,一旦符合UIViewControllerRepresentable便可將其使用於 UI。

protocol UIViewControllerRepresentable : View where Self.Body == Never

接著,在 ImagePicker 底下創建兩個必要的方法:makeUIViewController()updateUIViewController()。在makeUIViewController()裡必須創建一個所需的 UIImagePickerController 並回傳。至於updateUIViewController()裡則可留白,暫無需使用。

struct ImagePicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
        // 暫且留白
    }
}

補充:此處有個較便捷的方式可產生這兩個 function,就是在 struct 底下先鍵入以下這段文字,然後在 Xcode 跳出的 alert message 中點按 fix 即可。

struct ImagePicker: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIImagePickerController
}

完成以上步驟後,ImagePicker 算是擁有一半的功能,也就是能讓使用者從照片圖庫中選取圖片,但選完之後 SwiftUI 就不知下一步該做什麼。所以,必須接著完成前面 UIKit 基本認識中所說的第3點 - delegate。而 UIKit 所採用的 delegate 解決方案,在 SwiftUI 裡被稱為 coordinator,它是一個 object 專門負責對其綁定對象的事件做出反應。

接著,在 ImagePicker 底下創建一個 custom class,取名為 "Coordinator"。同時,新增方法 makeCoordinator(),並在其中產生一個 Coordinator 的實例。然後,在makeUIViewController()中,將 coordinator 指派給 picker 的 delegate。最後,SwiftUI 會檢查我們所創建的 Coordinator class 是否符合相應的 protocol(NSObjectUINavigationControllerDelegateUIImagePickerControllerDelegate),所以還必須將其補上。

class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    // more code
}

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

func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
    let picker = UIImagePickerController()
    picker.delegate = context.coordinator
    return picker
}

最後,在使用者選取完圖片時,通常 UI 便會同步關閉選取視窗,並將所選的圖片通知其接手的 view。因此,我們還必須加上 presentationMode 的 Environment 變數,和存放所選圖片的 Binding 變數。另外,在 Coordinator class 裡,也必須在初始化時通知其所對應的對象為何。

struct ImagePicker: UIViewControllerRepresentable {
    @Environment(\.presentationMaode) var presentationMode
    @Binding var image: UIImage?

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        var parent: ImagePicker

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

    func makeCoordinator() {
        Coordinator(self)
    }
    
    // ...略過程式碼
}

至此,我們創建了一個包裹著 UIImagePickerController 的 struct,並可將其儲存為獨立的 swift 檔案,如此便能將其重複使用於類似的情境中。

Comments