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
}
}
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.
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.
Related
I'm trying to connect a GoBack button and a Forward button to a Webview. I've confirmed that it works fine in the webview (myWebView) which is set on the main view (myView).
In this webview, I implemented the ability to communicate(Java) with the webpage to open a sheet modal window(this is also web page).
However, the problem arises that the webview page return to goHome(reload to 1st page) whenever the modal window is dismissed (no problem when to open).
Isn't there any way to control this Reload Issue that occurs whenever the modal window dismiss?
If 'uiView.load(request)' is put something conditional on the updateUIView part, the goBack/Forward navigation buttons will fail.
struct myView: View {
#State var activeSheet: Bool = false
.....
var body: some View {
let webView = myWebView(web: nil, actSheet: $activeSheet, req: URLRequest(url: URL(string: "https://google.com")!))
VStack {
webView
}
.sheet(isPresented: $activeSheet) {
anotherView(urlToLoad: urlToOpenInSheet)
}
.onChange(of: urlToOpenInSheet) { newValue in
print("url changed to \(urlToOpenInSheet)!")
}
HStack {
Button(action: {
webView.goBack()
print("goBack: \(urlToOpenInSheet)")
}) {
Image(systemName: "arrow.backward")
.font(.system(size: 20))
.foregroundColor(.white)
.frame(alignment: .trailing)
}.frame(alignment: .topTrailing)
.padding(EdgeInsets(top: 5))
}
}
}
struct myWebView: UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
#Binding var activeSheet: Bool
init(web: WKWebView?, actSheet: Binding<Bool>, req: URLRequest) {
self.webview = WKWebView()
self._activeSheet = actSheet
self.request = req
}
....
....
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webView?.goBack()
}
func goForward(){
webView?.goForward()
}
....
....
}
Aim
To display a web view inside a SwiftUI view
Reload the contents of the web view when a button (SwiftUI) is pressed.
My Attempt
I have made an attempt to do the above.
Pass a Publisher (CurrentValueSubject) to the UIViewRepresentable
Observe for any new values from the publisher
If the value is true refresh the web view
Question:
Is there a better way to do this? or is my approach reasonable?
Code:
WebView
import SwiftUI
import WebKit
import Combine
struct WebView: UIViewRepresentable {
let url: URL
let shouldRefresh: CurrentValueSubject<Bool, Never>
func makeUIView(context: Context) -> some UIView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: configuration)
let request = URLRequest(url: url)
context.coordinator.navigation = webView.load(request)
context.coordinator.webView = webView
context.coordinator.observeChanges(for: shouldRefresh)
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print("update view called")
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
extension WebView {
class Coordinator {
var navigation: WKNavigation?
var webView: WKWebView?
private var cancellable: AnyCancellable?
#MainActor
func observeChanges(for shouldRefresh: CurrentValueSubject<Bool, Never>) {
cancellable = shouldRefresh
.filter { $0 == true }
.sink { [weak self] newValue in
self?.navigation = self?.webView?.reload()
}
}
}
}
ContentView
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
VStack {
Button("Reload") {
print("reload pressed")
reload()
}
WebView(url: URL(string: "https://www.apple.com")!,
shouldRefresh: model.shouldRefresh)
}
}
func reload() {
model.shouldRefresh.send(true)
}
}
extension ContentView {
class Model: ObservableObject {
var shouldRefresh = CurrentValueSubject<Bool, Never>(false)
}
}
I am trying to use a UIViewController representable in a swiftUi project. Specifically I am trying to press one button (assetOne) that allows the EU to select a video and then press another button (assetTwo) and it allows the user to select another video. Then the user will have the option to merge the videos (with a third button). I assumed that I would need to use a Coordinator to accomplish this but after seeing a SO solution without it I tried to do it without one. But when I run my project the build is successful but when I click on any of the buttons from the content view I get the error message below. What am I doing wrong? Do I need a Coordinator and how do I incorporate it with my current configuration?
Warning: Attempt to present <UIImagePickerController: 0x7fa05f827600>
on <TempTest.MergeVideoViewController: 0x7fa05ed088c0> whose view is
not in the window hierarchy!
Content View:
import SwiftUI
struct ContentView: View {
let someView = ImagePicker()
var body: some View {
VStack {
Button(action: {
print("SwiftUI: assetOne button tapped")
// Call func in SomeView()
self.someView.assetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
//...
ImagePicker: UIViewControllerRepresentable
struct ImagePicker: UIViewControllerRepresentable{
let someView = MergeVideoViewController()
func makeUIViewController(context: Context) -> MergeVideoViewController {
someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
func assetOne() {
someView.loadAssetOne()
}
//...
}
My UIViewController class:
class MergeVideoViewController: UIViewController {
var firstAsset: AVAsset?
var secondAsset: AVAsset?
var audioAsset: AVAsset?
var loadingAssetOne = false
var activityMonitor: UIActivityIndicatorView!
func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
activityMonitor.stopAnimating()
firstAsset = nil
secondAsset = nil
audioAsset = nil
//...
func loadAssetOne() {
// func loadAssetOne(_ sender: AnyObject) {
if savedPhotosAvailable() {
loadingAssetOne = true
VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)
}
}
//...
The ImagePicker is-a View, it should be somewhere in body.
Here is possible approach - the idea is to get controller reference back in SwiftUI and call its actions directly when needed.
struct ImagePicker: UIViewControllerRepresentable{
let configure: (MergeVideoViewController) -> ()
func makeUIViewController(context: Context) -> MergeVideoViewController {
let someView = MergeVideoViewController()
configure(someView)
return someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
}
struct ContentView: View {
#State private var controller: MergeVideoViewController?
var body: some View {
VStack {
ImagePicker {
self.controller = $0
}
Button(action: {
print("SwiftUI: assetOne button tapped")
self.controller?.loadAssetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
}
}
I try to load a url in my WebView, it works well with "https://www.google.com", but it doesn't work with "https://www.sl.se".
It shows "Not Found, HTTP Error 404: The requested resource is not found."
The weird thing is that, if I google "sl.se" in Google page, click the sl.se, the the "www.sl.se" can be load the WebView. Does anyone know the reason?
ContentView.swift
// ContentView.swift
struct ContentView: View {
#State private var shouldRefresh = false
var body: some View {
VStack{
Button(action: {
self.shouldRefresh.toggle()
}){
Text("Reload")
}
WebView(url: nil, reload: $shouldRefresh)
}
}
}
WebView.Swift
// WebView.swift
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable{
var url: URL? // optional, if absent, one of below search servers used
#Binding var reload: Bool
private let urls = [URL(string: "https://google.com/")!, URL(string: "https://www.sl.se")!]
private let webview = WKWebView()
fileprivate func loadRequest(in webView: WKWebView) {
if let url = url {
webView.load(URLRequest(url: url))
} else {
let index = Int(Date().timeIntervalSince1970) % 2
print("load: \(urls[index].absoluteString)")
webView.load(URLRequest(url: urls[index]))
}
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
loadRequest(in: webview)
return webview
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
if reload {
loadRequest(in: uiView)
DispatchQueue.main.async {
self.reload = false // must be async
}
}
}
}
There is nothing wrong with your code. You just need to remove the www from your URL. try https://sl.se and It'll work.
When creating the WKWebView I pass the web page URL via #Binding webPageURL.
I also update the displayed website via webPageURL.
The problem here is that when I call a NavigationLink and navigate back from the NavigationLink, the updateUIView method is called and the WKWebView reloads the web page.
How do I prevent the WKWebView from calling the method updateUIView when I navigate back from the NavigationLink?
struct MainView:View {
#State private var isActive = false
#Binding var stateWebPageURL:String
var body: some View {
return VStack {
MyWKWebView(webPageURL:$stateWebPageURL)
NavigationLink(destination: SecoundDestination(),isActive: $isActive) {
Text("Do Something")
}
}
}
}
struct MyWKWebView: UIViewRepresentable {
#Binding var webPageURL:String
func updateUIView(_ uiView: WKWebView, context: Context) {
let pageURL = URL(string:webPageURL)
let urlRequest = URLRequest(url: pageURL!)
uiView.load(urlRequest)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero,configuration: webConfiguration)
let pageURL = URL(string:webPageURL)
let urlRequest = URLRequest(url: pageURL!)
webView.load(urlRequest)
return webView
}
func makeCoordinator() -> ContentController {
ContentController()
}
}
Provided code is not testable, so just by code reading, the idea is to make explicitly MyWKWebView equatable, as below
var body: some View {
return VStack {
MyWKWebView(webPageURL:$stateWebPageURL).equatable() // << here !!
and
struct MyWKWebView: UIViewRepresentable, Equatable {
static func == (lhs: MyWKWebView, rhs: MyWKWebView) -> Bool {
lhs.webPageURL == rhs.webPageURL
}
// ... other your code
SwiftUI should not update equal views, so should work.