メモ > 技術 > 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()
}
}
}
}
#Preview {
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)
}
}
}
#Preview {
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/")
}
}
#Preview {
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)
}
}
#Preview {
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 + "]")
}
)
}
}
}
#Preview {
ContentView()
}
■引っ張って更新
【SwiftUI】Pull to refresh(UIRefreshControl)を実装する - .NET ゆる〜りワーク
https://www.yururiwork.net/archives/1534
SwiftUI3.0より前は、「引っ張って更新」には対応していなかったが、現在は対応している。
実装方法は「SwiftUIその他」を参照。