SwiftUI UIViewRepresentable PDFKit PDFView AttributeGraph: cycle detected through attribute - pdfkit

Edit:
Created a sample project illustrating the issue:
https://github.com/Harold-D/PDFView_Representable
Question:
I'm at a lost, I have this very simple UIViewRepresentable wrapper around PDFView in SwiftUI
import PDFKit
import SwiftUI
struct MyPDFView: UIViewRepresentable {
typealias UIViewType = PDFView
#Binding var pdf: Data
func makeUIView(context _: UIViewRepresentableContext<MyPDFView>) -> UIViewType {
return PDFView()
}
func updateUIView(_ pdfView: UIViewType, context _: UIViewRepresentableContext<MyPDFView>) {
pdfView.document = PDFDocument(data: pdf)
}
}
It displays the PDF correctly, but produces about 18 AttributeGraph: cycle detected through attribute messages.
#Binding var pdfStruct: PDFStruct
var body: some View {
MyPDFView(pdf: Binding($pdfStruct.pdf)!)
}
struct PDFStruct {
var pdf: Data?
}
How can I eliminate the retain cycle?

Related

Reload web view contents when SwiftUI button is pressed

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)
}
}

SwiftUI View init called multiple times

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.

Simplest webview app in iOS - fail to run

I am trying to make an ios app that simply open a URL. So basically a webview app. I am using XCode 12.4 and that's what I did:
Create new project->iOS App --> (see image below)
Create a new file: ViewController.swift -->
import WebKit
class WebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://developer.apple.com")!
webView.load(URLRequest(url: url))
}
}
Build and Run.
The issue is that the app continues to open on "Hello World" and I imagine the reason is that the MyappApp.swift file is calling ContentView instead of ViewController
import SwiftUI
#main
struct myappApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
How can I fix this?
Disclaimer: This is my first iOS app and I know little to nothing about iOS dev.
You are very close... Wrap your WebViewController in a UIViewControllerRepresentable to make it compatible with SwiftUI
import SwiftUI
struct WebView_UI: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let vc = WebViewController()
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
And then reference your new SwiftUI UIViewControllerRepresentable in a SwiftUI struct. For example...
struct myappApp: App {
var body: some Scene {
WindowGroup {
WebView_UI()
}
}
}
Or...
struct ContentView: View {
var body: some View {
VStack{
Text("Hello World!").padding()
WebView_UI()
}
}
}

Sheet not dismissing with presentation mode in SwiftUI/UIKit?

I have a SwiftUI view that displays a sheet using a #State variable:
import SwiftUI
struct AdRevenue: View {
#State var playAd = false
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
self.playAd = true
})
{
Text("Play Ad")
}.sheet(isPresented: $playAd) {
Ads()}
}
}
This is the UIViewRepresentable sheet:
struct Ads: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return ViewController()
}
func updateUIViewController(_ uiView: UIViewController, context: Context) {
}
class ViewController: UIViewController, GADRewardedAdDelegate, AdManagerRewardDelegate {
var rewardedAd: GADRewardedAd?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
override func viewDidLoad() {
super.viewDidLoad()
AdManager.shared.loadAndShowRewardAd(AdIds.rewarded.rawValue, viewController: self)
AdManager.shared.delegateReward = self
}
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received: \(reward.type), amount \(reward.amount).")
}
}
}
Within AdManager, a function is called as such:
func rewardAdDidClose() {
let mom = Ads()
mom.presentationMode.wrappedValue.dismiss()
print("mom.presentationMode.wrappedValue.dismiss()")
}
Yet although I see the presentationMode message when I run it, the sheet doesn't get dismissed. Is it possible to dismiss the sheet like this?
Is this because you have two Ads values?
The first is created by SwiftUI (inside the AdRevenue view), so its presentationMode property is likely to be correctly wired-up to the app's environment.
But later, within AdManager, you're creating another Ads value, and expecting it to have a usable presentationMode environment object. It's being created outside of SwiftUI, so cannot know about the environment of the rest of your app.
I'd try passing the Ads value into your AdManager, rather than having AdManager create a new one.

Change SwiftUI Text from UIViewController

I'm quite new to Swift and absolute new to SwiftUI.
I'm trying to combine UIViewController Google Maps and modern SwiftUI.
In SwiftUI i have a few Text objects, and I want my GmapsController class to be able to modify these Text values, and redraw SwiftUI struct.
My main struct:
var _swiftUIStruct : swiftUIStruct = swiftUIStruct() // to have access in GmapsController
struct GoogMapView: View {
var body: some View {
let gmap = GmapsControllerRepresentable()
return ZStack {
gmap
_swiftUIStruct
}
}
}
UIViewControllerRepresentable wrapper of GmapsController :
struct GmapsControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) -> GmapsController {
return GmapsController()
}
func updateUIViewController(_ uiViewController: GmapsController, context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) {}
}
The GmapsController itself:
class GmapsController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(
withLatitude: (locationManager.location?.coordinate.latitude)!,
longitude: (locationManager.location?.coordinate.longitude)!,
zoom: 15
)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.delegate = self
self.view = mapView
}
// HERE I WANT TO CHANGE SOME swiftUIStruct Text FIELDS
// calls after map move
func mapView(_ mapView: GMSMapView, idleAt сameraPosition: GMSCameraPosition) {
_swiftUIStruct.someTxt = "I hope this will work" // compilation pass, but value doesn't change
}
}
And the swiftUIStruct struct:
struct swiftUIStruct {
#State var someTxt = ""
var body: some View {
Text(someTxt) // THE TEXT I WISH I COULD CHANGE
}
}
Googling this a whole day just made me feel dumb, any help is appreciated.
I hope my example code helps. Basically, move the model data outside, and pass it along, and change it. If you run this code, you will see the text "I hope this will work", NOT "Initial Content".
import SwiftUI
class ViewModel: ObservableObject {
#Published var someTxt = "Initial Content"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ZStack {
GoogleMapsView(viewModel: viewModel)
Text(viewModel.someTxt)
}
}
}
struct GoogleMapsView: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> GmapsController {
let controller = GmapsController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: GmapsController, context: Context) {}
}
class GmapsController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
viewModel?.someTxt = "I hope this will work"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}

Resources