Memo

メモ > 技術 > IDE: Xcode > SwiftUI+UIViewRepresentable

■SwiftUI+UIViewRepresentable
■SwiftUIには無いWebKitの機能を呼び出す UIKitで設計されたものをSwiftUIで使用するときは、「〇〇Representable」に準拠させる 具体的には、UIView は UIViewRepresentable に、UIViewController は UIViewControllerRepresentable に準拠させる 【SwiftUI】UIViewRepresentableの使い方!Coordinatorクラスとは? https://tech.amefure.com/swift-uiviewrepresentable 【SwiftUI】UIKitで作成したUIViewControllerやUIViewをSwiftUI側で表示する方法 - NRIネットコムBlog https://tech.nri-net.com/entry/display_uiview_created_with_uikit_on_swiftui UIKitのUIViewController/UIViewをSwiftUIで利用する場合の利用方法とその詳細 - Qiita https://qiita.com/yimajo/items/791dc1c1693d9821c5a8 SwiftUIで対応しきれずUIKitを使ったコンポーネントのまとめ - スタディサプリ Product Team Blog https://blog.studysapuri.jp/entry/2022/03/28/using-uikit-in-swiftui SwiftUI と UIKit 混合環境で開発を行うときの tips 集 - Qiita https://qiita.com/AkkeyLab/items/732887517da9abab6634 SwiftUIでAVFundationを導入する【Video Capture偏】 https://blog.personal-factory.com/2020/06/14/introduce-avfundation-by-swiftui/ 例えばSwiftUIにはWebViewが無い この場合、UIViewRepresentableを継承してWebKitを呼び出すことでSwiftUIから利用できる このファイルを「UIViewRepresentable」で検索すると、この項目の他にもいくつかの例がある UIViewRepresentableを使ってUIKitのviewをSwiftUI上で扱う|Tamappe Life Log https://tamappe.com/2020/08/08/uiviewrepresentable/ ■サンプル1(ボタンから制御されるラベルを自作) UIKitのUIViewをSwiftUIから利用する場合、UIViewRepresentableプロトコルに準拠して実装する 具体的には、以下のようにmakeUIView(表示するViewの初期状態のインスタンスを生成)とupdateUIView(表示するビューの状態が更新されるたびに呼び出され更新を反映)を実装する LabelView.swift
import SwiftUI struct LabelView: UIViewRepresentable { @Binding var isClick: Bool func makeUIView(context: Context) -> UILabel { let labelView: UILabel = UILabel() labelView.text = "UIKitから作成したView" labelView.textAlignment = NSTextAlignment.center return labelView } func updateUIView(_ uiView: UILabel, context: Context) { if isClick { uiView.text = "変更しました。" }else{ uiView.text = "UIKitから作成したView" } } }
ContentView.swift
import SwiftUI struct ContentView: View { @State var isClick: Bool = false var body: some View { VStack { LabelView(isClick: $isClick).padding() Button("ボタン"){ isClick.toggle() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
■サンプル2(ボタン側も自作&ラベルは上のものを流用) UIKitのイベントをSwiftUIで管理する場合、Coordinatorクラスを定義する (このクラス名に決まりは無いが、「Coordinator」という名前にしておくのが無難) ButtonView.swift
import SwiftUI struct ButtonView: UIViewRepresentable { @Binding var isClick: Bool func makeUIView(context: Context) -> UIButton { let control = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) control.setTitle("ボタン", for: .normal) control.setTitleColor(.red, for: .normal) control.addTarget(context.coordinator,action: #selector(Coordinator.clickButton(sender:)),for: .touchUpInside) return control } func updateUIView(_ uiView: UIButton, context: Context) { } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator { var control: ButtonView init(_ control: ButtonView){ self.control = control } @objc func clickButton(sender : Any){ control.isClick.toggle() } } }
ContentView.swift
import SwiftUI struct ContentView: View { @State var isClick: Bool = false var body: some View { VStack { LabelView(isClick: $isClick).padding() ButtonView(isClick: $isClick) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
■WebView(UIViewRepresentableの例) SwiftUIにはWebViewが無い UIViewRepresentableで機能を作成する SwiftUIでWebViewを使う - Qiita https://qiita.com/wiii_na/items/36123cf901839a8038e2 SwiftUIでUIViewを表示する https://zenn.dev/yorifuji/articles/swiftui-uiviewrepresentable UIViewRepresentableを使ってSwiftUIでちょっとリッチなWebviewを表示してみる - Qiita https://qiita.com/k_awoki/items/448fd0bd6f51500d13b1 WebView.swift
import SwiftUI import WebKit struct WebView: UIViewRepresentable { var loadUrl: String func makeUIView(context: Context) -> WKWebView { return WKWebView() } func updateUIView(_ uiView: WKWebView, context: Context) { uiView.load(URLRequest(url: URL(string: loadUrl)!)) } }
ContentView.swift
import SwiftUI struct ContentView: View { var body: some View { WebView(loadUrl: "https://www.apple.com/jp/") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
■WebView(UIViewRepresentableの例&高機能版) UIViewRepresentableを使ってSwiftUIでちょっとリッチなWebviewを表示してみる - Qiita https://qiita.com/k_awoki/items/448fd0bd6f51500d13b1 ContentView.swift
import SwiftUI struct ContentView: View { let url = URL(string: "https://www.apple.com/jp/")! var body: some View { WebView(url: url) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
WebView.swift
import SwiftUI struct WebView: View { // 表示するURL let url: URL // アクション @State private var action: WebContentView.Action = .none // 戻れるか @State private var canGoBack: Bool = false // 進めるか @State private var canGoForward: Bool = false // ローディング中か @State private var isLoading: Bool = false // 読み込みの進捗状況 @State private var loadingProgress: Double = 0.0 // ページタイトル @State private var pageTitle: String = "Now Loading..." var body: some View { NavigationView { VStack(spacing: 0) { if isLoading { WebProgressBarView(loadingProgress: loadingProgress) } WebContentView( url: url, action: $action, canGoBack: $canGoBack, canGoForward: $canGoForward, isLoading: $isLoading, loadingProgress: $loadingProgress, pageTitle: $pageTitle ).navigationBarTitle(Text(pageTitle), displayMode: .inline) WebToolBarView( action: $action, canGoBack: canGoBack, canGoForward: canGoForward ) } } // iPadでも画面全体に表示する .navigationViewStyle(StackNavigationViewStyle()) } }
WebContentView.swift
import SwiftUI import WebKit struct WebContentView: UIViewRepresentable { // 表示するURL let url: URL // アクション @Binding var action: Action // 戻れるか @Binding var canGoBack: Bool // 進めるか @Binding var canGoForward: Bool // ローディング中か @Binding var isLoading: Bool // 読み込みの進捗状況 @Binding var loadingProgress: Double // ページタイトル @Binding var pageTitle: String // WebViewのアクション enum Action { case none case goBack case goForward case reload } // 表示するView private let webView = WKWebView() func makeUIView(context: Context) -> WKWebView { webView.navigationDelegate = context.coordinator webView.load(URLRequest(url: url)) return webView } func updateUIView(_ uiView: WKWebView, context: Context) { switch action { case .goBack: uiView.goBack() case .goForward: uiView.goForward() case .reload: uiView.reload() case .none: break } action = .none } func makeCoordinator() -> WebContentView.Coordinator { return Coordinator(parent: self) } static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) { coordinator.observations.forEach({ $0.invalidate() }) coordinator.observations.removeAll() } } extension WebContentView { final class Coordinator: NSObject, WKNavigationDelegate { let parent: WebContentView var observations: [NSKeyValueObservation] = [] init(parent: WebContentView) { self.parent = parent let progressObservation = parent.webView.observe(\.estimatedProgress, options: .new, changeHandler: { _, value in parent.loadingProgress = value.newValue ?? 0 }) let isLoadingObservation = parent.webView.observe(\.isLoading, options: .new, changeHandler: { _, value in parent.isLoading = value.newValue ?? false }) observations = [ progressObservation, isLoadingObservation ] } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { parent.canGoBack = webView.canGoBack parent.canGoForward = webView.canGoForward parent.pageTitle = webView.title ?? "" } } }
WebToolBarView.swift
import SwiftUI struct WebToolBarView: View { // アクション @Binding var action: WebContentView.Action // 戻れるか var canGoBack: Bool // 進めるか var canGoForward: Bool var body: some View { VStack() { Divider() HStack(spacing: 16) { Button(action: { action = .goBack }) { Image(systemName: "arrow.backward") }.disabled(!canGoBack) Button(action: { action = .goForward }) { Image(systemName: "arrow.forward") }.disabled(!canGoForward) Spacer() Button(action: { action = .reload }) { Image(systemName: "arrow.clockwise") } } .padding(.top, 8) .padding(.horizontal, 16) Spacer() }.frame(height: 60) } }
WebProgressBarView.swift
import SwiftUI struct WebProgressBarView: View { // 読み込みの進捗状況 var loadingProgress: Double var body: some View { VStack { GeometryReader { geometry in Rectangle() .foregroundColor(Color.gray) .opacity(0.3) .frame(width: geometry.size.width) Rectangle() .foregroundColor(Color.blue) .frame(width: geometry.size.width * CGFloat(loadingProgress)) } }.frame(height: 2.0) } }
■TextField付きAlertを表示する(UIViewControllerRepresentableの例) SwiftUIのアラートにはテキスト入力の機能が無い UIViewControllerRepresentableで機能を作成する 【SwiftUI】TextField付きAlertを表示する - .NET ゆる〜りワーク https://www.yururiwork.net/%E3%80%90swiftui%E3%80%91textfield%E4%BB%98%E3%81%8Dalert%E3%82%92%E8%A1%... TextFieldAlertView.swift
import SwiftUI struct TextFieldAlertView: UIViewControllerRepresentable { @Binding var text: String @Binding var isShowingAlert: Bool let placeholder: String let isSecureTextEntry: Bool let title: String let message: String let leftButtonTitle: String? let rightButtonTitle: String? var leftButtonAction: (() -> Void)? var rightButtonAction: (() -> Void)? func makeUIViewController(context: UIViewControllerRepresentableContext<TextFieldAlertView>) -> some UIViewController { return UIViewController() } func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext<TextFieldAlertView>) { guard context.coordinator.alert == nil else { return } if !isShowingAlert { return } let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) context.coordinator.alert = alert alert.addTextField { textField in textField.placeholder = placeholder textField.text = text textField.delegate = context.coordinator textField.isSecureTextEntry = isSecureTextEntry } if leftButtonTitle != nil { alert.addAction(UIAlertAction(title: leftButtonTitle, style: .default) { _ in alert.dismiss(animated: true) { isShowingAlert = false leftButtonAction?() } }) } if rightButtonTitle != nil { alert.addAction(UIAlertAction(title: rightButtonTitle, style: .default) { _ in if let textField = alert.textFields?.first, let text = textField.text { self.text = text } alert.dismiss(animated: true) { isShowingAlert = false rightButtonAction?() } }) } DispatchQueue.main.async { uiViewController.present(alert, animated: true, completion: { isShowingAlert = false context.coordinator.alert = nil }) } } func makeCoordinator() -> TextFieldAlertView.Coordinator { Coordinator(self) } class Coordinator: NSObject, UITextFieldDelegate { var alert: UIAlertController? var view: TextFieldAlertView init(_ view: TextFieldAlertView) { self.view = view } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let text = textField.text as NSString? { self.view.text = text.replacingCharacters(in: range, with: string) } else { self.view.text = "" } return true } } }
ContentView.swift
import SwiftUI struct ContentView: View { @State private var isShowingAlert = false @State var text: String = "" var body: some View { VStack { Text("SwiftUIからUIKitの命令を呼び出す") .padding() Button("TextField付きAlertを表示する") { isShowingAlert = true } TextFieldAlertView( text: $text, isShowingAlert: $isShowingAlert, placeholder: "", isSecureTextEntry: true, title: "ログイン", message: "パスワードを入力してください", leftButtonTitle: "キャンセル", rightButtonTitle: "認証", leftButtonAction: nil, rightButtonAction: { print("パスワード認証リクエスト [" + text + "]") } ) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
■引っ張って更新 【SwiftUI】Pull to refresh(UIRefreshControl)を実装する - .NET ゆる〜りワーク https://www.yururiwork.net/archives/1534 SwiftUI3.0より前は、「引っ張って更新」には対応していなかったが、現在は対応している 実装方法は「SwiftUIその他」を参照

Advertisement