Detecting whether a WKWebView has blanked - swift

I am building a simple iOS app using SWIFT. It loads a single page app in WKWebView in the main view full screen. Clicking advertisement would open another WKWebView in a new scene presented modally. However, if I click several pages in the new scene and go back to the main view, the main view has a 50% chance to go blank.
When the main view goes blank, it disappears from the Safari inspector and webView.reload() doesn't work. But loadHTMLString works. So I can do something like this:
1. override func viewWillAppear(animated: Bool) {
2. super.viewWillAppear(false)
3. if pageStatus == .BackFromAdScene {
4. if webView is Blank {
5. loadFromLocal()
6. }
7. }
8. }
I'm curious about what is the simplest way to detect whether a WKWebView is Blank? In other words, how should I write line 4?

In Objective-c I did it with this trick:
[self.webView evaluateJavaScript:#"document.querySelector('body').innerHTML" completionHandler:^(id result, NSError *error) {
if (!result || ([result isKindOfClass:[NSString class]] && [((NSString *)result) length] == 0)) {
// reload your page
}];

user3437673's answer is correct. Here's the SWIFT code for people like me, who came to iOS development just recently.
self.webView?.evaluateJavaScript("document.querySelector('body').innerHTML") { (result, error) in
if error != nil {
// the main web view has turned blank
// do something like reload the page
}
}

Related

Accessing Current Page Information in Safari 12 Extension Popover

I'm trying to get the active tab's page information to be displayed in a popover. I'm able to get it to show the URL or Title of the page currently, but it requires opening and closing the extension popover twice.
I believe it's because to get that information, the below code relies on completionHandler which makes it async.
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage(completionHandler: { (page) in
page?.getPropertiesWithCompletionHandler( { (properties) in
self.pageTitle = properties?.title ?? "Unknown"
self.ActivePageTitle.stringValue = self.pageTitle
})
})
}
}
The first time you open the popover it shows a blank text region, but the second time it will have loaded in the information before and shows it correctly.
I've tried running it in viewDidLoad() but that only fires the first time the popover is opened.
When running it in viewWillAppear() I get the below error:
pid(17738)/euid(501) is calling TIS/TSM in non-main thread environment,
ERROR : This is NOT allowed. Please call TIS/TSM in main thread!!!
I thought maybe switching the extension to use a command instead would work but then realized you can't programatically open the popover window.
Do I have to switch to UITableView or something that has a reloadData() function to run once the async request for data is complete?
MacOS 10.14.4 | Safari 12.1 | Xcode 10.2
Try updating your UI code on the main thread. Wrap the page title updates in DispatchQueue. I had almost the exact same issue, and this worked for me.
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage(completionHandler: { (page) in
page?.getPropertiesWithCompletionHandler( { (properties) in
DispatchQueue.main.async {
self.pageTitle = properties?.title ?? "Unknown"
self.ActivePageTitle.stringValue = self.pageTitle
}
})
})
}
}
To give proper credit, I found the answer while digging through this code on GitHub (check out the updateDataLabels() method):
https://github.com/otzbergnet/tabCount/blob/master/tabCount%20Extension/SafariExtensionViewController.swift
You can get SFSafariPageProperties object in SafariExtensionHandler and use this object in SafariExtensionViewController.
- (void)popoverWillShowInWindow:(SFSafariWindow *)window {
[window getActiveTabWithCompletionHandler:^(SFSafariTab *activeTab) {
[activeTab getActivePageWithCompletionHandler:^(SFSafariPage *page) {
[page getPagePropertiesWithCompletionHandler:^(SFSafariPageProperties *properties) {
// Now you can use "properties" in viewController using shareObject
}];
}];
}];
}
Now need to get properties of SFSafariPage again in viewWillAppear.

How to check if UIViewController is already being displayed?

I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}

(UIApplication.shared.keyWindow?.rootViewController as? BaseSlidingController)?.openMenu() return nil Swift 4.2

I have a problem with the above stated. I can not find the exact information on the forums. Most of them are outdated and I have written the code programmatically. I have a controller that contains a view to edit the profile. I can not access that after changing the function listed below. I have a rootviewcontroller set to something else, but I tried the UiApplication calls anyway and it return nil and I can not open the profile controller. This is the function listed below.
#objc func handleOpen2() {
(UIApplication.shared.keyWindow?.rootViewController as? BaseSlidingController)?.openMenu()
}
Xcode does not give me an error but I can not get my menu to open. My rootviewcontroller is set to something else in app delegate. I have a controller that is used to control the sliding menu when I press the edit profile button.
func openMenu() {
isMenuOpened = true
redViewLeadingConstraint.constant = menuWidth
redViewTrailingConstraint.constant = menuWidth
performAnimations()
setNeedsStatusBarAppearanceUpdate()
}
This code is used to open my side bar menu with the information I need and also to perform animations as well. I was wondering if someone had any idea what I can do different instead in my handleOpen2 function. If you need more code, please let me know. Thanks
On swift 5 version:
(UIApplication.shared.windows.first?.rootViewController as? BaseSlidingController)?.openMenu()

OSX Showing Storyboard View Controller Programatically - Swift

So i have this app and on the first open i want it to show a quick start page. The code i have to initiate and work out if its there first open is
let savedData = NSUserDefaults(suiteName: "xxx")
if savedData?.boolForKey("firstTimeUser") == nil {
savedData?.setObject(true, forKey: "firstTimeUser")
savedData?.synchronize()
print("Hello")
}
if savedData?.boolForKey("firstTimeUser") == true {
//Code to show view controller! Help :)
savedData?.setBool(false, forKey: "firstTimeUser")
savedData?.synchronize()
}
I have a NSViewController on my main.storyboard and i want it to present it self as a sperate window that the user can close after they have finished reading it.
Im thinking i have to use a storyboard id but iv gotten lost and need help!
Thanks in advance!

What is the Swift version of the command to AirPrint on the *iPad*?

Thanks to a kind stackoverflow member I now have AirPrint(ing) working perfectly from the iPhone, but the function call for the iPad is giving me fits. Here is what I have:
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
pic.presentFromRect(self.view.frame, inView:self.view, animated:true, completionHandler: nil)
} else {
pic.presentAnimated(true, completionHandler: nil)
}
The line that calls the print dialog for the iPad is my Swift conversion of Objective-C code that I found numerous times through my Google searching on the topic. It runs and gets called correctly, but all it does is appear to "dim" the iPad screen on the simulator (like something is opening, but it's just a huge grey transparent rectangle that covers the entire screen). If I click anywhere on the dimmed screen it returns to normal and the program continues as if I never pressed my "Print" button (I am using my own "Print" button I created in the Storyboard). I can only test this on the simulator currently, so it could even be a beta bug for all I know... but I'm guessing I'm doing something wrong.
Any suggestions are appreciated!
Thanks.
Thanks to Aaron, my printer select pop-up for the iPad now works on every button in my program except the print button! :) Here is the relevant code:
#IBOutlet var myButton1: BorderedButton!
#IBOutlet var myButton2: BorderedButton!
#IBOutlet var myButton3: BorderedButton!
Works perfectly on all but button 3, which of course is my "Print" button. Here is the function for button 3:
#IBAction func button3Tapped() {
var pic:UIPrintInteractionController = .sharedPrintController()
var viewpf:UIViewPrintFormatter = myTextView.viewPrintFormatter()
pic.delegate = self
pic.showsPageRange = true
pic.printFormatter = viewpf
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
pic.presentFromRect(self.myButton3.frame, inView:self.view, animated:true, completionHandler: nil)
} else {
pic.presentAnimated(true, completionHandler: nil)
}
}
Change that "self.myButton3.frame" to myButton1 or myButton2 and I get the printer select pop-up in the iPad simulator with no problems. But making it myButton3 just gives me the error "fatal error: unexpectedly found nil while unwrapping an Optional value". I'm willing to let it appear on one of the other buttons if I have no other choice, but it would probably be good for me to understand what in the heck is happening!
Thanks again!
Either your outlet for myButton3 isn't connected to your button (try reconnecting it) or it's getting set to nil later, in which case it should be declared as optional: #IBOutlet var myButton3: BorderedButton?.