SwiftUI: Do not refresh a custom view MacOS app - ios

Hello I want to create an application with several webview that once they have been loaded once, I don't want them to be recreated anymore I want them to keep their instance.
I create in my application A navigation view which includes a list of 10 sites. When I click on a item of the list I want to display the corresponding webview with NavigationLink. It works. But the problem is that if I click on another item of the list and I will return to the previous one it returns to the home page, the webview loads again. I want the webview to be created only once and always stay alive in as long as the app is alive. I know that swiftui always refresh views is the problem, how can I prevent my webview from being refreshed by swiftUI?
In uitkit it's simple I create an array of wkwebview at launch of the app, I load all my webview url, in a singleton class. And depending on the item selected from my tableview I display the corresponding wkwebview. And even if I change the item all my webview are alive even if we don't see them.
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
let web = WKWebView()
web.load(request)
return web
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}

Here is simplified demo of possible approach. The idea is to cache created WKWebView objects by some identifiers and just reuse them in created (or recreated) thin-wrapper SwiftUI view representable.
Tested & works with Xcode 11.2 / iOS 13.2
struct WebView: UIViewRepresentable {
private static var cache: [Int: WKWebView] = [:]
// the only allowed entry point to create WebView, so have control either
// to create new instance of WKWebView or provide already loaded
static func view(with id: Int, request: URLRequest) -> WebView {
var web = cache[id] // it is UI thread so safe to access static
if web == nil {
web = WKWebView()
cache[id] = web
}
return WebView(with: web!, request: request)
}
private init(with web: WKWebView, request: URLRequest) {
self.web = web
self.request = request
}
private let web: WKWebView
private let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
if web.url == nil { // just created, so perform initial loading
web.load(request)
}
return web
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
// Just simple test view
struct TestOnceLoadedWebView: View {
private let urls: [String] = [
"http://www.apple.com",
"http://www.google.com",
"http://www.amazon.com"
]
var body: some View {
NavigationView {
List(0 ..< urls.count) { i in
NavigationLink("Link \(i)", destination:
WebView.view(with: i, request:
URLRequest(url: URL(string: self.urls[i])!)))
}
}
}
}

Related

Initializing issue between Binding (in WebView) and State (in View)

This issue is an initialization problem encountered in compiling code to transport variables between web views and views. I've asked 3 months ago(previous question was asked here.). I haven't been able to solve it yet. I gave up on solving and found for other alternatives, but wanted to know the cause or solution to the issue, so I re-identified the issue and recreated the question.
I'm implementing a sheet between a Web view and a View. The values ​​in the sheet are always changing. The content of the sheet called from the view must be shared by the web view (or reverse). There is no problem when mounting the web view directly inside the view. But the issue occurs when using with 'let' expressions. The following error message appears right before compilation:
"...Cannot use instance member '$activeSheet' within property initializer; property initializers run before 'self' is available..."
I haven't only used init(){...} in the view and but also used various initializing methods, but the initialization error message does not disappear. The most cause of this issue seems to be a problem with init, which sends #State from the view to web view and requests the value of #Binding down from the Web view to View again.
Is there any way?
struct ItlnrView: View {
#State var activeSheet: **Bool** = false
.....
let webView = myWebView(web: nil, actSheet: **$activeSheet**, req: URLRequest(url: URL(string: "https://google.com")!)). <--- problem
var body: some View {
VStack {
webView
// myWebView(web: nil, actSheet: $activeSheet, req: URLRequest(url: URL(string: "https://google.com")!)) <--- no problem
}
.sheet(isPresented: $activeSheet) {
// code
}
}
}
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
}
....
....
}
If I don't use let webView and run myWebView(web:,actSheet:,req:) in V/HStack directly, the sheet value is compatible/shared properly. But I must use/call by let webView because I have to use the 'go back' and 'reload' functions of Web view. So whenever I use let webView, the initialization error message occurs on $activeSheet.
Anyway, this Web view must be use instance member $activeSheet within property initializer through let webView....
I hope you to be understand my lacked question.
I solved it through Lorem Ipsum advised ('Put the webview in the body') and it works fine.
struct ItlnrView: 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) {
// code
}
}
}
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
}
....
....
}

Swift Webkit Webview with WebRTC produces a Black Video Screen

I am having an issue that only occurs with Swift, Webkit abd WebRTC. I am creating a webview of this url:
https://widgets.bingewave.com/webrtc/6b8bcc00-864d-4c97-8e15-0b3aed37946b
The complete code is here: https://github.com/BingeWave/bingewave-swift-video-app
The code for creating the Webview is this:
Webview
import SwiftUI
import WebKit
struct SwiftUIWebView: UIViewRepresentable {
let url: URL?
func makeUIView(context: Context) -> CustomWebView {
//Preferences
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
//Create a congif
let config = WKWebViewConfiguration()
//Add
config.defaultWebpagePreferences = prefs
return CustomWebView(
frame: .zero,
configuration: config
)
}
func updateUIView(_ uiView: CustomWebView, context: Context) {
guard let myURL = url else {
return
}
let request = URLRequest (url: myURL)
uiView.load(request)
}
CustomWebView.swift
//
// CustomWebView.swift
// BingWave Sample App Swift
//
// Created by Ayusma on 07/04/2022.
//
import WebKit
class CustomWebView: WKWebView {
//Change th Auth Token with that of a user
let AUTH_TOKEN = "xxxxxxx";
override func load(_ request: URLRequest) -> WKNavigation? {
guard let mutableRequest: NSMutableURLRequest = request as? NSMutableURLRequest else {
return super.load(request)
}
mutableRequest.setValue(AUTH_TOKEN, forHTTPHeaderField: "Authorization")
return super.load(mutableRequest as URLRequest)
}
}
ContentView
import SwiftUI
struct ContentView: View {
let BINGWAVE_URL = URL(string: "https://widgets.bingewave.com/webrtc/6b8bcc00-864d-4c97-8e15-0b3aed37946b")
var body: some View {
NavigationView{
SwiftUIWebView(url: BINGWAVE_URL)
.navigationTitle("BingWave Sample App")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The issue is the screen loads the view, I am able to join the video chat but my camera video black and if another user joins, their video is a black within the app. What is weird is they can see me and themsevles on their end, so the video is only black on the iOS app. And this works for other implements like a React Native Webview, it only fails for this implementation with Swift.
Am I missing anything?

PHPickerViewController tapping on Search gets error... "Unable to load photos"

I'm trying to implement a PHPickerViewController using SwiftUI and The Composable Architecture. (Not that I think that's particularly relevant but it might explain why some of my code is like it is).
Sample project
I've been playing around with this to try and work it out. I created a little sample Project on GitHub which removes The Composable Architecture and keeps the UI super simple.
https://github.com/oliverfoggin/BrokenImagePickers/tree/main
It looks like iOS 15 is breaking on both the UIImagePickerViewController and the PHPickerViewController. (Which makes sense as they both use the same UI under the hood).
I guess the nest step is to determine if the same error occurs when using them in a UIKit app.
My code
My code is fairly straight forward. It's pretty much just a reimplementation of the same feature that uses UIImagePickerViewController but I wanted to try with the newer APIs.
My code looks like this...
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
All this works in the simple case
I can present the ImagePicker view and select a photo and it's all fine. I can cancel out of it ok. I can even scroll down the huge collection view of images that I have. I can even see the new image appear in my state object and display it within my app. (Note... this is still WIP and so the code is a bit clunky but that's only to get it working initially).
The problem case
The problem is that when I tap on the search bar in the PHPickerView (which is a search bar provided by Apple in the control, I didn't create it or code it). It seems to start to slide up the keyboard and then the view goes blank with a single message in the middle...
Unable to Load Photos
[Try Again]
I also get a strange looking error log. (I removed the time stamps to shorten the lines).
// These happen on immediately presenting the ImagePicker
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: still loading) with error: (null)
AppName[587:30596] Writing analzed variants.
// These happen when tapping the search bar
AppName[587:30867] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin interrupted while in use.
AppName[587:31002] [lifecycle] [u A95D90FC-C77B-43CC-8FC6-C8E7C81DD22A:m (null)] [com.apple.mobileslideshow.photospicker(1.0)] Connection to plugin invalidated while in use.
AppName[587:30596] [Picker] Showing picker unavailable UI (reason: crashed) with error: (null)
AppName[587:30596] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}
Tapping the "Try Again" button reloads the initial scroll screen and I can carry on using it. But tapping the search bar again just shows the same error.
I'm usually the first one to point out that the error is almost definitely not with the Apple APIs but I'm stumped on this one. I'm not sure what it is that I'm doing that is causing this to happen?
Is it the fact that it's in a SwiftUI view?
Recreated the project in UIKit
I remade the same project using UIKit... https://github.com/oliverfoggin/UIKit-Image-Pickers
And I couldn't replicate the crash at all.
Also... if you are taking any sort of screen recording of the device the crash will not happen. I tried taking a recording on the device itself and couldn't replicate it. I also tried doing a movie recording from my Mac using the iPhone screen and couldn't replicate the crash. But... the instant I stopped the recording on QuickTime the crash was replicable again.
This fixed it for me .ignoreSafeArea(.keyboard) like #Frustrated_Student mentions.
To elaborate on #Frustrated_Student this issue has to do with the UIViewControllerRepresentable treating the view like many SwiftUI views to automatically avoid the keyboard. If you are presenting the picker using a sheet as I am then you can simply add the .ignoreSafeArea(.keyboard) to the UIViewControllerRepresentable view in my case I called it ImagePicker here is a better example.
Where to add it the .ignoreSafeArea(.keyboard)
.sheet(isPresented: $imagePicker) {
ImagePicker(store: store)
.ignoresSafeArea(.keyboard)
}
This is #Fogmeister code:
public struct ImagePicker: UIViewControllerRepresentable {
// Vars and setup stuff...
#Environment(\.presentationMode) var presentationMode
let viewStore: ViewStore<ImagePickerState, ImagePickerAction>
public init(store: Store<ImagePickerState, ImagePickerAction>) {
self.viewStore = ViewStore(store)
}
// UIViewControllerRepresentable required functions
public func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> some UIViewController {
// Configuring the PHPickerViewController
var config = PHPickerConfiguration()
config.filter = PHPickerFilter.images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// This is the coordinator that acts as the delegate
public class Coordinator: PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let itemProvider = results.first?.itemProvider,
itemProvider.canLoadObject(ofClass: UIImage.self) else {
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self?.parent.viewStore.send(.imagePicked(image: image))
}
}
}
}
}
}
Well.. this seems to be an iOS bug.
I have cerated a sample project here that shows the bug... https://github.com/oliverfoggin/BrokenImagePickers
And a replica project here written with UIKit that does not... https://github.com/oliverfoggin/UIKit-Image-Pickers
I tried to take a screen recording of this happening but it appears that if any screen recording is happening (whether on device or via QuickTime on the Mac) this suppresses the bug from happening.
I have filed a radar with Apple and sent them both projects to have a look at and LOTS of detail around what's happening. I'll keep this updated with any progress on that.
Hacky workaround
After a bit of further investigation I found that you can start with SwiftUI and then present a PHPickerViewController without this crash happening.
From SwiftUI if you present a UIViewControllerRepresentable... and then from there if you present the PHPickerViewController it will not crash.
So I came up with a (very tacky) workaround that avoids this crash.
I first create a UIViewController subclass that I use like a wrapper.
class WrappedPhotoPicker: UIViewController {
var picker: PHPickerViewController?
override func viewDidLoad() {
super.viewDidLoad()
if let picker = picker {
present(picker, animated: false)
}
}
}
Then in the SwiftUI View I create this wrapper and set the picker in it.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.presentationMode.wrappedValue.dismiss()
parent.wrappedPicker.dismiss(animated: false)
parent.photoPickerResult = results.first
}
}
}
This is far from ideal as I'm presenting at the wrong time and stuff. But it works until Apple provide a permanent fix for this.
I started getting a weird UI bug after the PHPickerViewController crashed where the keyboard was not visible but my views were still being squashed. So I suspected a keyboard / avoidance issue. I disabled keyboard avoidance in a parent view and managed to stop it from crashing.
.ignoresSafeArea(.keyboard)
.... still a iOS bug in 15.0. I've modified Fogmeister's class Coordinator to return the image in addition to the PHPickerResult.
struct WrappedPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var photoPickerResult: PHPickerResult?
#Binding var image: UIImage?
let wrappedPicker = WrappedPhotoPicker()
func makeUIViewController(context: Context) -> WrappedPhotoPicker {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
wrappedPicker.picker = picker
return wrappedPicker
}
func updateUIViewController(_ uiViewController: WrappedPhotoPicker, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
let parent: WrappedPickerView
init(_ parent: WrappedPickerView) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
self.parent.presentationMode.wrappedValue.dismiss()
self.parent.wrappedPicker.dismiss(animated: false)
self.parent.photoPickerResult = results.first
print(results)
guard let result = results.first else {
return
}
self.parent.image = nil
DispatchQueue.global().async {
result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
guard let imageLoaded = object as? UIImage else {
return
}
DispatchQueue.main.async {
self.parent.image = imageLoaded
}
}
}
}
}
}

How to make WKWebView the first responder and respond to keyboard shortcuts after scrolling?

I've laid out a couple WKWebViews in SwiftUI (using UIViewRepresentable) for an iPad app.
When I simply click on one those views, it gets focus, and I can use various keyboard shortcuts specific to the website (in the example code below the pages use these DuckDuckGo shortcuts - https://help.duckduckgo.com/duckduckgo-help-pages/features/keyboard-shortcuts/ ), as well as up/down keys to scroll etc.
However, if I just scroll a web view, then it doesn't get focus.
How would I make the page get focus (becomeFirstResponder in UIKit terms) as soon as the web page is scrolled. Additionally, how could I show the focused web view in the UI (for instance, by making the web views border red)?
My code:
import WebKit
struct ContentView: View {
var body: some View {
VStack {
Webview(url: URL(string:"https://duckduckgo.com/?q=stack+overflow&t=h_&ia=web")!)
Divider()
Webview(url: URL(string:"https://duckduckgo.com/?q=ipad&t=h_&ia=web")!)
}
}
}
struct Webview: UIViewRepresentable {
let url: URL
func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
let webview = WKWebView()
let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
webview.load(request)
return webview
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<Webview>) {
let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad)
webview.load(request)
}
}

How to open a linked pdf with turbolinks-ios

I'm wondering how to open a linked pdf file with the turbolinks-ios framework in iOS.
Currently, I'm experiencing the issue that when a turbolinks page links to a pdf or other file, then the link will open in safari rather than the embedded view.
Background
The turbolinks-5 library together with the turbolinks-ios framework provide a way to connect a web application to the native navigation controllers of the corresponding mobile app.
The screenshot is taken from the turbolinks README.
Desired behavior
When clicking a link that refers to a pdf, a seaparate view controller should be pushed to the current navigation controller, such that the user can read the pdf and easily navigate back to the document index.
Observed behavior
The linked pdf is opened in safari rather than within the app. Unfortunately, safari asks for authentication, again. Furthermore, the user has to leave the application.
Intercept the click of the pdf link
For a link to a pdf file, the didProposeVisitToURL mechanism is not triggered for the session delegate. Thus, one can't decide from there how to handle the linked pdf.
Instead, one could intercept clicking the link by becoming turbolinks' web view's navigation delegate as shown in the README:
extension NavigationController: SessionDelegate {
// ...
func sessionDidLoadWebView(session: Session) {
session.webView.navigationDelegate = self
}
}
extension NavigationController: WKNavigationDelegate {
func webView(webView: WKWebView,
decidePolicyForNavigationAction navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> ()) {
// This method is called whenever the webView within the
// visitableView attempts a navigation action. By default, the
// navigation has to be cancelled, since when clicking a
// turbolinks link, the content is shown in a **new**
// visitableView.
//
// But there are exceptions: When clicking on a PDF, which
// is not handled by turbolinks, we have to handle showing
// the pdf manually.
//
// We can't just allow the navigation since this would not
// create a new visitable controller, i.e. there would be
// no back button to the documents index. Therefore, we have
// to create a new view controller manually.
let url = navigationAction.request.URL!
if url.pathExtension == "pdf" {
presentPdfViewController(url)
}
decisionHandler(WKNavigationActionPolicy.Cancel)
}
}
Present the pdf view controller
Similarly to presenting the visitable view as shown in the turbolinks-ios demo application, present the pdf view controller:
extension NavigationController {
func presentPdfViewController(url: NSURL) {
let pdfViewController = PdfViewController(URL: url)
pushViewController(pdfViewController, animated: true)
}
}
Or, if you'd like to show other file types as well, call it fileViewController rather than pdfViewController.
PdfViewController
The new view controller inherits from turbolinks' VisitableViewController to make use of the initialization by url.
class PdfViewController: FileViewController {
}
class FileViewController: Turbolinks.VisitableViewController {
lazy var fileView: WKWebView = {
return WKWebView(frame: CGRectZero)
}()
lazy var filename: String? = {
return self.visitableURL?.pathComponents?.last
}()
override func viewDidLoad() {
view.addSubview(fileView)
fileView.bindFrameToSuperviewBounds() // https://stackoverflow.com/a/32824659/2066546
self.title = filename // https://stackoverflow.com/a/39022302/2066546
fileView.loadRequest(NSURLRequest(URL: visitableURL))
}
}
To get the web view to the correct size, I used bindFrameToSuperviewBounds as shown in this stackoverflow answer, but I'm sure there are other methods.
Optional: Sharing cookies
If loading the pdf needs authentication, it's convenient to share the cookies with the turbolinks-ios webview as described in the README.
For example, create a webViewConfiguration which can be passed to the pdfViewController:
extension NavigationController {
let webViewProcessPool = WKProcessPool()
lazy var webViewConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.processPool = self.webViewProcessPool
// ...
return configuration
}()
lazy var session: Session = {
let session = Session(webViewConfiguration: self.webViewConfiguration)
session.delegate = self
return session
}()
}
The same webViewConfiguration needs to be passed to the session (shown above) as well as to the new pdf view controller.
extension NavigationController {
func presentPdfViewController(url: NSURL) {
let pdfViewController = PdfViewController(URL: url)
pdfViewController.webViewConfiguration = self.webViewConfiguration
pushViewController(pdfViewController, animated: true)
}
}
class FileViewController: Turbolinks.VisitableViewController {
var webViewConfiguration: WKWebViewConfiguration
lazy var fileView: WKWebView = {
return WKWebView(frame: CGRectZero, configuration: self.webViewConfiguration)
}()
// ...
}
Demo

Resources