Accessing Current Page Information in Safari 12 Extension Popover - swift

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.

Related

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

Currently messing with Swift Reachability, should I do all my code in viewDidLoad or in viewDidAppear?

I'm currently trying to implement Reachability into my current project. I followed a tutorial on YouTube that worked but I'm unsure whether or not its the correct way of doing it. In the Reachability documentation (https://github.com/ashleymills/Reachability.swift) it shows two examples first one being 'Example - closures' where I assume it's done in the viewDidLoad?
//declare this property where it won't go out of scope relative to your listener
let reachability = Reachability()!
reachability.whenReachable = { reachability in
// this is called on a background thread, but UI updates must
// be on the main thread, like this:
DispatchQueue.main.async {
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
}
}
reachability.whenUnreachable = { reachability in
// this is called on a background thread, but UI updates must
// be on the main thread, like this:
DispatchQueue.main.async {
print("Not reachable")
}
}
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
and the last example was 'Example - notifications', this is where I get confused the creator says to do that all in viewDidAppear. Is there really a big difference if I just do everything inside viewDidLoad? Does it change the outcome of anything? It currently works fine but I'm not sure whether it's right, I don't want it affecting me in the future. Any help would be great! Thanks.
It depends on your needs.
If you want to use Reachability...
... dynamically only if this particular view is frontmost, startNotifier() in viewWillAppear and stopNotifier() in viewDidDisappear.
... in this particular view as long as the view is alive/loaded startNotifier() in viewDidLoad.
... globally in all views put the entire code in AppDelegate and post notifications.

Check if NSAlert is currently showing up

I am using an NSAlert to display error messages on the main screen of my app.
Basically, the NSAlert is a property of my main view controller
class ViewController: NSViewController {
var alert: NSAlert?
...
}
And when I receive some notifications, I display some messages
func operationDidFail(notification: NSNotification)
{
dispatch_async(dispatch_get_main_queue(), {
self.alert = NSAlert()
self.alert.messageText = "Operation failed"
alert.runModal();
})
}
Now, if I get several notifications, the alert shows up for every notification. I mean, it shows up with the first message, I click on "Ok", it disappears and then shows up again with the second message etc... Which is a normal behaviour.
What I would like to achieve is to avoid this sequence of error message. I actually only care about the first one.
Is there a way to know if my alert view is currently being displayed ?
Something like alert.isVisible as on iOS's UIAlertView ?
From your code, I suspect that notification is triggered in background thread. In this case, any checks that alert is visible right now will not help. Your code will not start subsequent block execution until first block will finish, because runModal method will block, running NSRunLoop in modal mode.
To fix your problem, you can introduce atomic bool property and check it before dispatch_async.
Objective-C solution:
- (void)operationDidFail:(NSNotification *)note {
if (!self.alertDispatched) {
self.alertDispatched = YES;
dispatch_async(dispatch_get_main_queue(), ^{
self.alert = [NSAlert new];
self.alert.messageText = #"Operation failed";
[self.alert runModal];
self.alertDispatched = NO;
});
}
}
Same code using Swift:
func operationDidFail(notification: NSNotification)
{
if !self.alertDispatched {
self.alertDispatched = true
dispatch_async(dispatch_get_main_queue(), {
self.alert = NSAlert()
self.alert.messageText = "Operation failed"
self.alert.runModal();
self.alertDispatched = false
})
}
}
Instead of run modal you could try
- beginSheetModalForWindow:completionHandler:
source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSAlert_Class/#//apple_ref/occ/instm/NSAlert/beginSheetModalForWindow:completionHandler:
In the completion handler set the alert property to nil.
And only show the alert if the alert property is nil ( which would be every first time after dismissing the alert).
EDIT : I don't see the documentation say anything about any kind of flag you look for.

Detecting whether a WKWebView has blanked

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