0
votes

I am pretty new to SwiftUI. I have a very simple view. It's just a root view that contains a WKWebView wrapped in a UIViewRepresentable. My problem is, that the init method of the UIViewRepresentable is called 6 times when the view is opened. Which means the WKWebView is initialised 6 times and all my initialisation code (setting JS callbacks, ...) is called 6 times. I added print statements to the init functions of the root view MyWebView and the subview WebView (the UIViewRepresentable). The root view init is only called once, but the subview's init is called 6 times. Is this normal? Or am I doing something wrong?

struct MyWebView: View {

@ObservedObject private var viewModel = WebViewModel()

init() {
    print("root init")
}

var body: some View {
        VStack(alignment: .leading, spacing: 0, content: {
            WebView(viewModel: viewModel)
        })
        .navigationBarTitle("\(viewModel.title)", displayMode: .inline)
        .navigationBarBackButtonHidden(true)
} }

struct WebView: UIViewRepresentable {

var wkWebView: WKWebView!

init(viewModel: WebViewModel) {
    print("webview init")
    doWebViewInitialization(viewModel: viewModel)
}

func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
    let request = URLRequest(url: URL(string: "https://www.google.com")!, cachePolicy: .returnCacheDataElseLoad)
    wkWebView.load(request)

    return wkWebView
}

}

2
do you call and change something in your "viewModel" somewhere else in your code?workingdog
@workingdog: No, the ViewModel is only doing business logic, nothing view related.D.D.

2 Answers

2
votes

I'm not getting your issue of multiple calls to the init method of the UIViewRepresentable. I modified slightly your WebView, and this is how I tested my answer:

import SwiftUI
import Foundation
import WebKit

@main
struct TestSApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MyWebView()
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

// for testing
class WebViewModel: ObservableObject {
    @Published var title = ""
}

struct WebView: UIViewRepresentable {
    let wkWebView = WKWebView()
    
    init(viewModel: WebViewModel) {
        print("\n-----> webview init")
      //  doWebViewInitialization(viewModel: viewModel)
    }
    
    func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
        if let url = URL(string: "https://www.google.com") {
            let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
            wkWebView.load(request)
        }
        return wkWebView
    }
    
    func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}

struct MyWebView: View {
    @ObservedObject private var viewModel = WebViewModel()
    
    init() {
        print("\n-----> root init")
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0, content: {
            WebView(viewModel: viewModel)
        })
            .navigationBarTitle("\(viewModel.title)", displayMode: .inline)
            .navigationBarBackButtonHidden(true)
    }
}

This leaves "doWebViewInitialization" with a possible problem spot.

1
votes

You have to write your code assuming that the initializer of the View in SwiftUI will be called many times.

You write the initialization process in makeUIView(context:) in this case.

See: https://developer.apple.com/documentation/swiftui/uiviewrepresentable/makeuiview(context:)


For example, I wrote the following code based on this answer. I added a toggle height button to this referenced code.

the -----> makeUIView log is only output once, but the -----> webview init logs are output every time the toggle button is pressed.

import SwiftUI
import WebKit

struct ContentView: View {
  var body: some View {
    MyWebView()
  }
}

class WebViewModel: ObservableObject {
  @Published var title = ""
}

struct WebView: UIViewRepresentable {
  let wkWebView = WKWebView()

  init(viewModel: WebViewModel) {
    print("\n-----> webview init")
  }

  func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
    print("\n-----> makeUIView")
    if let url = URL(string: "https://www.google.com") {
      let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
      wkWebView.load(request)
    }
    
    return wkWebView
  }

  func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}

struct MyWebView: View {
  @State private var toggleHight = false
  @ObservedObject private var viewModel = WebViewModel()

  init() {
    print("\n-----> root init")
  }

  var body: some View {
    VStack {
      WebView(
        viewModel: viewModel
      )
      .frame(
        height: { toggleHight ? 600 : 300 }()
      )

      Button(
        "toggle",
        action: {
          toggleHight.toggle()
        }
      )
    }
  }
}

Furthermore, I realized after I wrote example code that WebView: UIViewRepresentable should not have an instance variable of wkWebView.

Please do it all(create instance and configuration) in makeUIView(context:), as shown below. This is because instance variables are recreated every time the initializer is called.

import SwiftUI
import WebKit

struct ContentView: View {
  var body: some View {
    MyWebView()
  }
}

class WebViewModel: ObservableObject {
  @Published var title = ""
}

struct WebView: UIViewRepresentable {
  init(viewModel: WebViewModel) {
    print("\n-----> webview init")
  }

  func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
    print("\n-----> makeUIView")
    let wkWebView = WKWebView()
    if let url = URL(string: "https://www.google.com") {
      let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
      wkWebView.load(request)
    }

    return wkWebView
  }

  func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}

struct MyWebView: View {
  @State private var toggleHight = false
  @ObservedObject private var viewModel = WebViewModel()

  init() {
    print("\n-----> root init")
  }

  var body: some View {
    VStack {
      WebView(
        viewModel: viewModel
      )
      .frame(
        height: { toggleHight ? 600 : 300 }()
      )

      Button(
        "toggle",
        action: {
          toggleHight.toggle()
        }
      )
    }
  }
}

I struggled with this tight constraint when I was developing with UIViewControllerRepresentable. With the help of my colleagues, I managed to finish the code.


Your code has been called 6 times, so there may be some problem. but I cannot tell what the problem is from the code you provided.

It is common for init to be called multiple times in SwiftUI. We need to write code to deal with this. If your init is being called too often, you may want to look for the root cause. The code I referred to and the code I wrote are only once at startup.