As of iOS14, SwiftUI does not currently support View equivalent to WKWebView. Therefore, it is necessary to wrap and use WKWebView of UIKit for SwiftUI. This time, the goal is to display a WebView equipped with basic functions (toolbar, progress bar), and I would like to summarize how to use ʻUIViewRepresentable` that appears in it.
To use UIKit's View in Swift UI, you need to use ʻUIViewRepresentable. ʻUIViewRepresentable
is a wrapper for using UIKit View in SwiftUI.
Describes each function defined in the ʻUIViewRepresentable` protocol.
func makeUIView(context: Self.Context) -> Self.UIViewType
Implementation required. Create an instance of the View to display. The View of UIKit used in SwiftUI is returned as a return value.
func updateUIView(Self.UIViewType, context: Self.Context)
Implementation required. Called when the state of the app is updated. If there is an update of View, describe it in this function.
static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)
Called when the specified UIKit View is deleted. Describe the cleanup process in this function, such as deleting the registered notification if necessary.
func makeCoordinator() -> Self.Coordinator
Implement when there is an event to be notified from the View side.
By defining Coordinator
, it becomes possible to perform event handling by user operation such as Delegate.
Now let's get into the main subject of WKWebView display.
The first is to simply display the WebView in Swift UI.
ʻUIViewType will be ʻassociatedtype
, so change it to the View type of the UIKit you want to wrap.
This time, I will use WKWebView.
WebView.swift
struct WebView: UIViewRepresentable {
///View to display
private let webView = WKWebView()
///URL to display
let url: URL
func makeUIView(context: Context) -> WKWebView {
//Set the return value to WKWebView and return it
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
Next, we will create a toolbar with three elements: back`` forward
reload
.
First, create a toolbar and place each button.
Next, use Property Wrappers
to enable WebView to detect when each button is tapped on the toolbar.
If you tap the button on the toolbar in this state, the state of the application will be updated and ʻupdateUIView (_ uiView :, context :) of ʻUIViewRepresentable
will be fired.
From the above, on the WebView side, describe the action according to the button in ʻupdateUIView (_ uiView :, context :)`.
WebView.swift
struct WebView: UIViewRepresentable {
//abridgement...
///WebView actions
enum Action {
case none
case goBack
case goForward
case reload
}
///action
@Binding var action: Action
func updateUIView(_ uiView: WKWebView, context: Context) {
///Called every time the bound value is updated
///When action is updated, process according to the updated value
switch action {
case .goBack:
uiView.goBack()
case .goForward:
uiView.goForward()
case .reload:
uiView.reload()
case .none:
break
}
action = .none
}
}
WebToolBarView.swift
struct WebToolBarView: View {
///action
@Binding var action: WebView.Action
var body: some View {
VStack() {
HStack() {
//Update actions according to the button you tap
Button("Back") { action = .goBack }
Button("Forward") { action = .goForward }
Button("Reload") { action = .reload }
}
}
}
}
RichWebView.swift
struct RichWebView: View {
/// URL
let url: URL
///action
@State private var action: WebView.Action = .none
var body: some View {
VStack() {
WebView(url: url,
action: $action)
WebToolBarView(action: $action)
}
}
}
Property Wrappers
such as @State
and @Binding
are not explained here because they deviate from the main subject.
There are many articles that explain in detail, so please refer to them separately if necessary.
State and Data Flow | Apple Developer DocumentationNow it is possible to process when the button is tapped in WebView. However, the buttons can be tapped in any state, so if you can not move to the previous or next page, deactivate each button.
** Define Coordinator **
Whether or not you can move to the previous or next page is determined when the WebView page load is completed.
To do this, you need to implement WKNavigationDelegate
, but you cannot implement it directly in WebView.
So in ʻUIViewRepresentable, you need to create a ** Coordinator
, a custom instance ** to tell SwiftUI the changes received from the ** UIKit View.
WKNavigationDelegate
is implemented in Coordinator
, and event handling on the Swift UI side is performed through it.
WebView.swift
struct WebView: UIViewRepresentable {
//abridgement...
///Can i go back
@Binding var canGoBack: Bool
///Do you want to proceed
@Binding var canGoForward: Bool
func makeCoordinator() -> WebView.Coordinator {
return Coordinator(parent: self)
}
}
extension WebView {
final class Coordinator: NSObject, WKNavigationDelegate {
///Parent View
let parent: WebView
init(parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.canGoBack = webView.canGoBack
parent.canGoForward = webView.canGoForward
}
}
}
After that, receive each value with WebToolBarView
via RichWebView
, and describe the inactivity and activation processing of each button, and you're done.
WebToolBarView.swift
Button("Back") { action = .goBack }.disabled(!canGoBack)
Button("Forward") { action = .goForward }.disabled(!canGoForward)
Finally, let's create a progress bar.
You need to get ʻestimated Progress and ʻis Loading
of WKWebView
to create a progress bar.
This time, we will use KVO for each.
You will receive a change notification from View, so use the Coordinator
you used earlier.
WebView.swift
struct WebView: UIViewRepresentable {
//abridgement...
///Loading progress
@Binding var estimatedProgress: Double
///Whether loading
@Binding var isLoading: Bool
static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
//Called when WKWebView is deleted
//Disable and delete notifications when the instance is deleted
coordinator.observations.forEach({ $0.invalidate() })
coordinator.observations.removeAll()
}
}
extension WebView {
final class Coordinator: NSObject, WKNavigationDelegate {
///Parent View
let parent: WebView
/// NSKeyValueObservations
var observations: [NSKeyValueObservation] = []
init(parent: WebView) {
self.parent = parent
//Register notifications
let progressObservation = parent.webView.observe(\.estimatedProgress, options: .new, changeHandler: { _, value in
parent.estimatedProgress = value.newValue ?? 0
})
let isLoadingObservation = parent.webView.observe(\.isLoading, options: .new, changeHandler: { _, value in
parent.isLoading = value.newValue ?? false
})
observations = [
progressObservation,
isLoadingObservation
]
}
//abridgement...
}
}
```
#### Create a progress bar
Now you have all the parts you need for a progress bar.
All you have to do is create `ProgressBarView.swift`, receive the value via` RichWebView` and display it.
#### **`ProgressBarView.swift`**
```swift
struct ProgressBarView: View {
///Loading progress
var estimatedProgress: 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(estimatedProgress))
}
}.frame(height: 3.0)
}
}
RichWebView.swift
struct RichWebView: View {
//abridgement...
///Loading progress
@State private var estimatedProgress: Double = 0.0
///Whether loading
@State private var isLoading: Bool = false
var body: some View {
VStack() {
if isLoading {
ProgressBarView(estimatedProgress: estimatedProgress)
}
//abridgement...
}
}
}
You have now implemented a set of functions. There are other points to consider such as error handling, but I think we were able to create a WebView with rough functions.
This time, I implemented it aiming at displaying a rich WebView, but in order to implement WebView with SwiftUI, it is necessary to create it using the basic functions of ʻUIViewRepresentable`, so it is just right for studying. think. If you are interested, please try it. I will summarize the source on github below, so if you are interested, I would appreciate it if you could see it. (The github side contains some code omitted here, such as the title display of the navigation bar and restrictions for layout.) RichWebViewSample
Also, if you have any suggestions for improvement or opinions regarding this implementation, we would appreciate it if you could comment. Thank you very much.
Recommended Posts