Search code examples
iosswiftswiftuiwkwebview

SwiftUI: Disable/Re-Enable button based on WebView state


I have a UIViewRepresentable wrapper around a WebView. I have added a bar beneath the webview with forward and back buttons. I want the buttons to be disabled when the WebView's canGoBack and canGoForward properties return false and vice versa.

ViewModel includes:

class ViewModel: ObservableObject {
  ...
   @Published var canGoBackPublisher = CurrentValueSubject<Bool, Never>(false)
   @Published var canGoForwardPublisher = CurrentValueSubject<Bool, Never>(false)
}

The ContentView includes:

struct ContentView: View {
   @ObservedObject var viewModel = ViewModel()
   ...
   
   var body: some View {
      VStack(spacing: 0) {
         WebView(viewModel: viewModel).overlay (
            RoundedRectangle(cornerRadius: 4, style: .circular)
               .stroke(Color.gray, lineWidth: 0.5)
         )
         WebNavigationView(viewModel: viewModel)
             .frame(minWidth: 0, maxWidth: .infinity, minHeight: 64, maxHeight: 64)
             .background(Color.white)
      }
      
   }

The WebNavigationView (the button bar) includes:

struct WebNavigationView: View {
   @ObservedObject var viewModel: ViewModel
   ...      
   
   var body: some View {
      HStack(alignment: .center, spacing: 64, content: {
         Button(action: goBack) {
            Image(systemName: "chevron.left").resizable().aspectRatio(contentMode: .fit)
            }.disabled(!viewModel.canGoBackPublisher.value).
                frame(width: 24, height: 24, alignment: .center).padding(.leading, 32)
         Button(action: goForward) {
            Image(systemName: "chevron.right").resizable().aspectRatio(contentMode: .fit)
            }.disabled(!viewModel.canGoForwardPublisher.value)
                .frame(width: 24, height: 24, alignment: .center)
         Spacer()
      })
   }
   ...

the WebView's delegate includes:

   func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
      parent.viewModel.canGoBackPublisher.send(webView.canGoBack)
      parent.viewModel.canGoForwardPublisher.send(webView.canGoForward)
   }

The buttons start up grayed-out and disabled as expected. But they don't react to the state change, they stay disabled even when viewModel.canGoBackPublisher.value returns true. I'm a longtime iOS developer but very, very, very new to SwiftUI


Solution

  • You're ending up doubling up the publisher property by defining them as @Published and CurrentValueSubject.

    The easiest fix would be to just make them Published, which handles most of the work for you:

    class ViewModel: ObservableObject {
       @Published var canGoBack = false
       @Published var canGoForward = false
    }
    
    //...
    //In delegate:
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
      viewModel.canGoBack = webView.canGoBack
      viewModel.canGoForward = webView.canGoForward
    }
    
    //...
    //In Navigation view:
    Button(action: goBack) {
      Image(systemName: "chevron.left").resizable().aspectRatio(contentMode: .fit)
    }.disabled(!viewModel.canGoBack) //<-- here
    .frame(width: 24, height: 24, alignment: .center).padding(.leading, 32)
    
    Button(action: goForward) {
      Image(systemName: "chevron.right").resizable().aspectRatio(contentMode: .fit)
    }.disabled(!viewModel.canGoForward) //<-- here
    .frame(width: 24, height: 24, alignment: .center)
    

    You could still define them as CurrentValueSubject if you want (and ditch the @Published property wrapper), but there's probably no need to in this case.

    Good SO question on the difference between @Published and CurrentValueSubject: Difference between CurrentValueSubject and @Published