忘年会シーズンに胃腸炎になってしまい全然お酒が飲めないかっくん(@fromkk)です。
社内向けに簡単なツールを作成しようと思いSwiftUIとCombineで作ってみたのですが、端末内にある写真を取得・表示する必要があったのでSwiftUIからUIKitのクラスを呼び出して画像を表示する方法を模索してみました。
UIImagePickerController
UIImagePickerControllerは昔からよく利用されている端末の画像を読み込む為のクラスです。
写真を数枚程度選択するぐらいならこちらで十分かと思います。
今回はこちらを利用してみます。
SwiftUI
SwiftUIは今年のWWDCで発表され、Xcode 11から利用可能な新しいUIの仕組みです。
iOS/watchOS/tvOS/macOS/macCatalystで利用が可能です。
宣言的に記述が可能になりこれまでのiOSでのUIの作り方とは随分と異なるのですが、まだまだ痒い所に手が届かない所があるので、そういう部分はこれまで利用してきた手法も利用する必要があります。
UIViewControllerのまま利用
まずは慣れ親しんだUIViewController
をpresent
して画像ピッカーを表示する手法を試してみました。
import UIKit import Combine final class ImagePicker: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate, ObservableObject { @Published var image: UIImage? func pick(on viewController: UIViewController) { let imagePickerViewController = UIImagePickerController() imagePickerViewController.sourceType = .photoLibrary imagePickerViewController.delegate = self viewController.present(imagePickerViewController, animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { defer { picker.dismiss(animated: true, completion: nil) } image = info[.originalImage] as? UIImage } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true, completion: nil) } }
こんな感じで ImagePicker
クラスを作成してみました。
続いてアプリ全体で参照される AppSettings
というクラスを作成し、
import UIKit final class AppSettings: ObservableObject { weak var containerViewController: UIViewController? }
そしてImagePickAndDisplayView
を作成しました。
import SwiftUI import Combine import UIKit struct ImagePickAndDisplayView: View { @EnvironmentObject var appSettings: AppSettings @ObservedObject var imagePicker = ImagePicker() var body: some View { VStack(alignment: .center, spacing: 16) { if imagePicker.image == nil { Rectangle() .frame(width: 300, height: 300, alignment: .top) } else { Image(uiImage: imagePicker.image!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 300, alignment: .top) .clipped() } Button(action: { self.imageSelect() }) { Text("Image Select") } } } func imageSelect() { guard let viewController = appSettings.containerViewController else { return } imagePicker.pick(on: viewController) } } struct ImagePickAndDisplayView_Preview: PreviewProvider { static var previews: some View { return ImagePickAndDisplayView() } }
この様なUIが出来上がったので、SceneDelegate.swift
で
var window: UIWindow? private let appSettings = AppSettings() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let contentView = ImagePickAndDisplayView() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) let rootViewController = UIHostingController(rootView: contentView.environmentObject(appSettings)) appSettings.containerViewController = rootViewController window.rootViewController = rootViewController self.window = window window.makeKeyAndVisible() } }
と設定しました。
肝はcontentView.environmentObject(appSettings)
の辺りでしょうか。これでImagePickAndDisplayView
側で @EnvironmentObject var appSettings: AppSettings
と定義しているだけで勝手に値が注入されます。便利。
結果はこんな感じです。
ただ、まだUIKitの手法に慣れすぎてSwiftUIっぽく無いなと感じたのでもう少しSwiftUIだけで表示する方法を模索してみます。
更にSwiftUIらしく
という事で UIImagePickerController
の表示部分をSwiftUIだけで実装してみます。
import SwiftUI import Combine import UIKit struct ImagePickerView: UIViewControllerRepresentable { @Binding var image: UIImage? final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @Binding var image: UIImage? init(image: Binding<UIImage?>) { _image = image } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { defer { picker.dismiss(animated: true) } guard let image = info[.originalImage] as? UIImage else { return } self.image = image } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } } func makeCoordinator() -> ImagePickerView.Coordinator { let coordinator = Coordinator(image: $image) return coordinator } func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerView>) -> UIImagePickerController { let imagePickerViewController = UIImagePickerController() imagePickerViewController.sourceType = .photoLibrary imagePickerViewController.delegate = context.coordinator return imagePickerViewController } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerView>) { // Nothing todo } }
肝はSwiftUI独自のCoordinatorという存在ですかね。
雑な理解だとSwiftUIの世界とUIKitの世界を繋げてくれる様な役割かと思います。
UIの表示部分は
import SwiftUI import Combine import UIKit struct ImagePickAndDisplaySwiftUIView: View { @State var image: UIImage? @State var showImagePicker: Bool = false var body: some View { VStack(alignment: .center, spacing: 16) { if image == nil { Rectangle() .frame(width: 300, height: 300, alignment: .top) } else { Image(uiImage: image!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 300, height: 300, alignment: .top) .clipped() } Button(action: { self.showImagePicker = true }) { Text("Image Select") } }.sheet(isPresented: $showImagePicker) { ImagePickerView(image: self.$image) } } } struct ImagePickAndDisplaySwiftUIView_Preview: PreviewProvider { static var previews: some View { return ImagePickAndDisplaySwiftUIView() } }
この様になりました。
結果の動きは先ほどと全く同じです。
この辺りを実装してみたものをhttps://github.com/fromkk/SwiftUI-Device-Imageに上げてみたので実際のコードが気になる方は参考にしてみてください。
まとめ
SwiftUIだけだと実装が難しい部分を既存の知識を利用して表現してみました。
方法によってはSwiftUIっぽく無い実装も可能ですが、あまり綺麗だとは言えないのでなるべくSwiftUIだけの世界に近い方法を目指していきたいですね。
更に良い方法などあれば教えて頂けるととても嬉しいです。
P.S.
少し凝ったView
を実装しようとすると
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
というエラーにとても悩まされました。1つ1つのコンポーネントをなるべく小さく作っていく癖が必要そうですね。
積極採用中!!
子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中!
急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP: http://timers-inc.com/engineerings
参考: ios - How to open the ImagePicker in SwiftUI? - Stack Overflow