WKWebView history serialization on iOS - ios

I'm trying to restore WKWebView navigation after app will restarts. I can inherit from base web view and take more control with it.
class WebViewHistory: WKBackForwardList {
// additional control with history items
// can perform custom init
}
class WebView: WKWebView {
var history: WebViewHistory
override var backForwardList: WebViewHistory {
return history
}
init(frame: CGRect, configuration: WKWebViewConfiguration, history: WebViewHistory) {
self.history = history
super.init(frame: frame, configuration: configuration)
}
}
For serialization I need serialize at least one WKBackForwardListItem. So here I face up with troubles.
class WebNavigationItem: WKBackForwardListItem {}
// but I cant create this objects
let item1 = WKBackForwardListItem() // 'init()' is unavailable
let item2 = WebNavigationItem() // 'WebNavigationItem' cannot be constructed because it has no accessible initializers
How to serialize history of WKWebView?
So for now I see only 1 option: make custom navigation with serialization support and forgot about allowsBackForwardNavigationGestures. And it required more code for implementing all logic of default web view history.

If you're targeting iOS 15 or above, you should look into WKWebView.interactionState.
It's erased to Any? but appears to be a plain old [NS]Data value behind the scenes. Maybe with luck you can serialise and restore that over app launches?

Related

Embed iOS UIViewController in a Flutter widget

I have a Flutter fullscreen modal widget with a header, a footer and some content which should be rendered natively for iOS. I know I can host iOS UIViews in Flutter using Platform Views and I managed to do all the logic to get this working.
My issue is that I need to host a whole view controller within this widget, not only a simple view, and this view controller belongs to a third-party framework.
An option would be implementing the header and footer natively, but this would take a lot of time since this would involve passing lot of data, performing network requests, adding callbacks and so on. I read online that a UIKitViewController exists, but it can only be created from PlatformViewServices, which is still a work in progress and should not be used. I didn't manage to find proper documentation online.
I think you can try this.
class NativeView: NSObject, FlutterPlatformView {
private var _vc: UIViewController
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
_vc = UIViewController()
super.init()
}
func view() -> UIView {
return _vc.view
}
}
Calling _vc.view will call loadView() and viewDidLoad() when view is not initialized yet.

QLPreviewController detect when QLPreviewItem Changed

I have application that use QLPreviewController to show document from MS. Office. I use this controller to multiple files. I need to log which files that already viewed. But I can't find event that triggered when PreviewItem changed.
I try to trigger event when GetPreviewItem called, But this method just called 1 time each preview item. So I can't use this.
Anyone can help me to find how to detect QLPreviewItem changed?
You can do this with Key-Value Observing. Subclass the QLPreviewController and add something like this:
var observation: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
observation = observe(\.currentPreviewItemIndex, options: [.new] ) { _, change in
// You get the new index value in change.newValue
}
}

WKWebView in iOS app takes a long time to load

I have a UIView that has a WKWebView as a subview. The web view shows up all right, but it takes a long time (more than a second) to display the HTML document. The document is a local file (an HTML file in the project) so there is no Internet latency, and it's a relatively simple HTML document. The HTML document does have eight small images on it, but there is a similar problem with another HTML document that doesn't have any images.
Here's the code that loads the HTML document into the web view:
override func viewDidLoad() {
super.viewDidLoad()
let localHtmlFile = Bundle.main.url(forResource: "place", withExtension: "html");
let request = URLRequest(url: localHtmlFile!);
webView.load(request);
}
I am using Xcode 9.3, Swift 4.1, and iOS 11.2.
The delay happens every time I go to that screen. If it isn't possible to prevent the delay the first time, is it possible to keep the web view around so that the delay only happens once?
Apparently the delay is caused by the time it takes to make a new instance of WKWebView, not the time it takes to load an HTML document. To avoid that delay I figured out a way to reuse a web view.
First I removed the web view from the storyboard scene so that a new web vew wouldn't be created every time the view was loaded. I made a generic view named container that is the same size that I wanted the web view to be.
Then I made a static variable to keep a pointer to the web view:
static var webView: WKWebView? = nil
In my case this static variable is in a class called GameController.
Next I changed the code to check to see if the static webView variable is nil. If webView is nil, the code creates a new web view and sets the static variable to point to that web view. Then the code programmatically adds the web view as a subview of a container view in the storyboard scene.
To set up the storyboard and write this code I used the explanation on the following web site:
http://www.onebigfunction.com/ios/2016/12/14/iOS-javascript-communication/
The basic code in the view controller for the scene that uses the web view (WebViewController in my code) looks like this:
override func loadView() {
super.loadView()
if GameController.webView == nil {
var webFrame = self.container!.frame
webFrame.origin.x = 0
webFrame.origin.y = 0
let config = WKWebViewConfiguration()
webView = WKWebView(frame: webFrame,
configuration: config)
GameController.webView = webView
} else {
webView = GameController.webView
}
self.container!.addSubview(webView)
}
In my case, I wanted to send information from JavaScript code in the web view to Swift code in my app, so I had to work more with configurations. I also wanted the web view to be transparent, so I added a statement to do that.
override func loadView() {
super.loadView()
if GameController.webView == nil {
var webFrame = self.container!.frame
webFrame.origin.x = 0
webFrame.origin.y = 0
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "scriptHandler")
webView = WKWebView(frame: webFrame,
configuration: config)
webView.isOpaque = false
GameController.webView = webView
} else {
webView = GameController.webView
webView.configuration.userContentController.removeScriptMessageHandler(
forName: "scriptHandler")
webView.configuration.userContentController.add(self,
name: "scriptHandler")
}
self.container!.addSubview(webView)
}
Originally I only set the script handler when I first made the web view, but that didn't work. Apparently a new view controller object was made each time the scene was loaded, so the old script handler didn't work. This code deletes the script handler that pointed to the old view controller and adds a script handler that points to the new view controller.
Just keep a reference to the ViewController, perhaps in a parent controller, app delegate, or even a singleton/global. It should be somewhat faster on subsequent loads.

Swift Save viewcontroller state using coder beyond app close

I am using Xcode 8 and swift 2.3
I want to save the entire view controller to file and restore state even after app closes.
I searched everywhere and found we need to use coder for that. but all just shows to save an object.
but here I need to save entire ViewContoller and subviews.
ViewCotroller will have three buttons
Add Text
Add Image : User can add any number of textViews and Images. So I need to save all that info also.
Add ViewController : User may have an array of this viewController and need to save all.
Question 1)
Can just save self.view and can it save all subviews automatically ?
Question 2)
I need to init without coder at start by just using
let nameVccVar = nameVcc()
and
let nameVccVar = nameVcc(coder: CodeVar)
Question 3)
How do I save all this coded data to file using NSKeyedUnarchiver and retrieve back?
Kindly help me or give me tips to make all this work
class nameVcc: UIViewController
{
var nameIntVar = 0
var nameStringVar = "Save this"
var nameImageVar = UIImage()
override func viewDidLoad()
{
super.viewDidLoad()
}
required init?(coder NkdPsgVar: NSCoder)
{
super.init(coder: NkdPsgVar)
}
override func encodeWithCoder(DkdPsgVar: NSCoder)
{
}
func addTextViewBtnClick()
{
let viewVar = UIView()
// Set many values for view
self.view.addSubview(viewVar)
}
func addImageViewBtnClick()
{
let imgViewVar = UIImageView()
// Set many values for ImageView
self.view.addSubview(imgViewVar)
}
}
I also tired :
convenience init()
{
self.init()
}
and
convenience init()
{
self.init(coder: NSCoder())
}
TL;DR:
There are key steps and methods required to implement state restoration in your app.
Opting-in for App State Restoration in App Delegate by returning true for these methods shouldSaveApplicationState and shouldRestoreApplicationState.
Setting Restoration Identifier for view controllers that you want state restoration implemented in.
Implementing encodeRestorableStateWithCoder and decodeRestorableStateWithCoder methods in your view controller. The former is used to save any state information of your view controller to disk using encodeObjectForKey method. The latter shall be used to restore the state back from your saved contents to the disk by using decodeObjectForKey method.
Watch this awesome blog post about State Restoration for easier grasp. If you have time, also do spend on watching State Restoration WWDC Session.
There is no need to save your entire ViewController yourself. UIKit does that for you when you set your Restoration Identifier (In Interface Builder). The only thing we need to focus for state restoration is to save your essential properties needed to re-create your app's "State", for example, a Bool property which determines whether you want to display a specific button or not.
Now coming to your series of questions....
Can I just save self.view and can it save all subviews automatically ?
You do not need to save any of your views. All subviews will be handled by UIKit. Encode your required properties (such as Bool flags, count variables, key properties that can be used to fetch data from api call for your datasource etc.) inside encodeRestorableStateWithCoder method. Don't forget to re-contruct your view controller's state from decodeRestorableStateWithCoder. Both of these methods belond to UIStateRestoring protocol.
I need to init without coder at start by just using
No need to do any fancy inits.
How do I save all this coded data to file using NSKeyedUnarchiver and
retrieve back?
As I said earlier, implement necessary UIStateRestoring protocol methods to save and restore your app's state.

Ensure that property observer didSet manipulates User Interface after viewDidLoad

I am working on an open source tutorial using MVVM, Coordinators and RxSwift. I am constructing all the viewcontrollers and models in the coordinator. Controller has a strong reference to viewmodel and when a viewmodel is set, I would like to perform some UI related actions(using property observer didSet). The problem I am facing is that didSet is called before viewDidLoad causing a crash.
Stripped down version of ViewController:
class MessageVC: UIViewController {
var viewModel: MessageViewModel! {
didSet {
manipulateUI() // crashes
}
}
override func viewDidLoad() {
super.viewDidLoad()
manipulateUI() // works fine if setup is correct in coordinator
}
Coordinator stripped down version:
extension AppCoordinator {
convenience init() {
let rootVC = MessageVC() // actual construction from storyboard
let messages = Message.getMessages()
rootVC.viewModel = MessageViewModel(withMessage: messages)
}
My concern is that even though calling manipulateUI in viewDidLoad is working for me currently, the app will crash if I forget to set the viewModel from my co-ordinator making me think that I am using a fragile architecture. I really like updating userinterface from didSet but it is called before viewDidLoad.
I know it is a simple problem but from architecture standpoint it seems fragile. Any suggestions, improvements and comments are appreciated a lot.
I wont say that cases like this can define wether you are dealing with fragile architecture or not because view controllers has their own life cycle which differs a lot from other objects life cycle. Anyway you can easily avoid crashes here using different approaches. For example :
Approach 1:
Put a guard statement at the very beginning of your manipulateUI function so this function wont manipulate UI until both view is loaded and model is set. Then call this function on viewDidLoad method and when viewModel is set:
func manipulateUI(){
guard let viewModel = self.viewModel , isViewLoaded else {
return
}
//continue manipulation here
}
Approach 2:
Since you are not sure wether view is loaded when you set the model and don't know if views are initialized yet, you can access the views as optional properties in manipulateUI function:
func manipulateUI(){
self.someLabel?.text = self.viewModel.someText
//continue manipulation here
}
Approach 3:
Since you are using RxSwift you can always register an observer for view controller's isViewLoaded property and set the data source after you are sure that view is loaded
Crash happens because at this point
rootVC.viewModel = MessageViewModel(withMessage: messages)
view controller is not initialized.
It won't work the way you're trying to accomplish, you have to call manipulateUI() inside viewDidLoad.

Resources