2
votes

I want to show a WKWebView in a SwiftUI view hierarchy, and I want to control the WKWebView using buttons implemented in SwiftUI.

I can do this by creating a SwiftUI WebView class that implements UIViewRepresentable and wraps WKWebView, like so...

struct WebView: UIViewRepresentable {

  func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
    return WKWebView(frame: .zero)
  }

  func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
    // Update WKWebView here if necessary
  }
}

...and then using it as follows...


struct BrowserView: View {
  var body: some View {
    VStack {
      WebView()

      // Footer
      HStack {
        Button(action: {
          // When this is called, I want to somehow call .goBack()
          // on the WKWebView.
        }) {
          Image(systemName: "chevron.left")
        }

        ...
      } // HStack
    } // VStack
  }
}

When the user taps the Button, I want to somehow call goBack on the WKWebView. How am I to do this idiomatically in SwiftUI?

Approaches I've considered that I think are either brittle or aren't idiomatic SwiftUI...

  • Represent the number of times that a user has clicked "Back" in a binding. Have WebView listen to that binding and move back when that increments.

  • Have BrowserView create an object called something like MutableWebViewAPI and pass it to WebView on construction. WebView listens on this object so that when BrowserView calls something like MutableWebViewAPI.goBack(), WebView does the same.

1

1 Answers

3
votes

I think the best way is to leverage the power of Combine by creating a PassthroughSubject on your parent view and then subscribe to events in your child WebView.

First, create a struct in your UIViewReprsentable that captures the available events you want to send to your WebView:

struct WebView: UIViewRepresentable {

    enum WebEvent {
        case back
        case forward
    }

    let eventPublisher: AnyPublisher<WebEvent, Never>

    init(eventPublisher: AnyPublisher<WebEvent, Never>) {
        self.eventPublisher = eventPublisher

        eventPublisher.sink { event in
            // forward the appropriate event to your underlying WKWebView
        }
    }
}

Next, in your parent, create the PassthroughSubject to create these events.

struct ParentView: View {
    private let eventSender = PassthroughSubject<WebView.WebEvent, Never>()

    var body: some View {
        VStack {
            Button("Back") {
                self.eventSender.send(.back)
            }
            Button("Forward") {
                self.eventSender.send(.forward)
            }

            WebView(eventPublisher: eventSender.eraseToAnyPublisher())
        }
    }
}