Swift: How to wrap an async method in a synchronous method? - ios

I'm trying to load Javascript synchronously in my WKWebView. From my research it looks like the evaluateJavaScript method is asynchronous and this is causing errors because the page would load before the JS that overrides a lot of behavior (it only works a portion of the time because of the async nature).
I've tried this but it doesn't seem to work :
extension WKWebView {
func evaluateJavaScriptSynchronously(script: String, completion: () -> Void) {
let semaphore = dispatch_semaphore_create(1)
evaluateJavaScript(script) { (result, error) in
if error == nil {
if result != nil {
print("SUCCESS")
completion()
}
} else {
print("ERROR!!!")
print(error)
print(result)
completion()
}
dispatch_semaphore_signal(semaphore)
}
let timeout = dispatch_time_t(10000)
dispatch_semaphore_wait(semaphore, timeout)
}
}
I know it's not working because the page still sometimes loads as if the JS wasn't there. Is there a similar thing I can do to synchronize the process?

You can define a WKNavigationDelegate to get notified when the page into the WKWebView has been loaded
class Controller: UIViewController, WKNavigationDelegate {
#IBOutlet var webView: WKWebView!
override func viewDidLoad() {
self.view = webView
webView.navigationDelegate = self
}
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
print("page loaded")
// <--- call your method from here
}
}
You can now call your method from within webView(wevView:didFinishNavigation:)

Related

OAuthSwift WKWebView opens and closes in a constant loop

I am trying to authenticate an iOS app with the Spotify API using OAuth2.
For this I am using OAuthSwift.
When my application loads, I am redirected to Spotify, I can log in and allow my app access to my account.
When I am redirected back to my app however, the WebView is dismissed, however immediately re opens on the previous page, dismissed itself and re opens.
This continues in a loop indefinitely.
I wondered if this is something todo with having my initAuthFlow function called in viewDidAppear, however moving this to viewDidLoad complains about
Warning: Attempt to present <OAuthKeyChainApp.WKWebViewController: 0x7fb42b505160> on <OAuthKeyChainApp.HomeController: 0x7fb42b50cf30> whose view is not in the window hierarchy!
and the controller is never presented.
HomeController.swift
class HomeController: OAuthViewController {
let oauthSwift = OAuth2Swift(
consumerKey: "xxxxxx",
consumerSecret: "xxxxxx",
authorizeUrl: "https://accounts.spotify.com/en/authorize",
accessTokenUrl: "https://accounts.spotify.com/api/token",
responseType: "code"
)
lazy var internalWebViewController: WKWebViewController = {
let controller = WKWebViewController()
controller.view = UIView(frame: UIScreen.main.bounds)
controller.loadView()
controller.viewDidLoad()
return controller
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .purple
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
initAuthFlow()
}
fileprivate func initAuthFlow() -> Void {
oauthSwift.authorizeURLHandler = internalWebViewController
guard let callbackURL = URL(string: "oauthkeychainapp://oauthkeychain-callback") else { return }
oauthSwift.authorize(
withCallbackURL: callbackURL,
scope: "user-library-modify",
state: generateState(withLength: 20),
success: { (credential, response, params) in
print(credential)
}) { (error) in
print(error.localizedDescription)
}
}
}
extension HomeController: OAuthWebViewControllerDelegate {
func oauthWebViewControllerDidPresent() { }
func oauthWebViewControllerDidDismiss() { }
func oauthWebViewControllerWillAppear() { }
func oauthWebViewControllerDidAppear() { }
func oauthWebViewControllerWillDisappear() { }
func oauthWebViewControllerDidDisappear() { oauthSwift.cancel() }
}
WKWebViewController.swift
import UIKit
import WebKit
import OAuthSwift
class WKWebViewController: OAuthWebViewController {
var webView: WKWebView!
var targetURL: URL?
override func viewDidLoad() {
super.viewDidLoad()
}
override func handle(_ url: URL) {
targetURL = url
super.handle(url)
loadAddressURL()
}
func loadAddressURL() {
guard let url = targetURL else { return }
let req = URLRequest(url: url)
self.webView?.load(req)
}
}
extension WKWebViewController: WKUIDelegate, WKNavigationDelegate {
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.allowsBackForwardNavigationGestures = true
webView.uiDelegate = self
webView.navigationDelegate = self
view = webView
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("loaded")
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Check for OAuth Callback
if let url = navigationAction.request.url, url.scheme == "oauthkeychainapp" {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
self.dismiss(animated: true, completion: nil)
decisionHandler(.cancel)
return
}
// Restrict URL's a user can access
if let host = navigationAction.request.url?.host {
if host.contains("spotify") {
decisionHandler(.allow)
return
} else {
// open link outside of our app
UIApplication.shared.open(navigationAction.request.url!)
decisionHandler(.cancel)
return
}
}
decisionHandler(.cancel)
}
}
You aren't doing anything to change the state of your application. Because of this initAuthFlow is being called again, Spotify I assume has a valid session for you, so the controller is dismissed and the cycle repeats.
In the success closure of your oauthSwift.authorize call you should put the tokens into the KeyChain or somewhere secure and ensure initAuthFlow is only called when that state is invalid.

Trying to implement a delegate and it isn't working [duplicate]

I have a view controller for a webview, setup through KINWebBrowser.
I've tried several ways to get these methods to execute:
func webViewDidStartLoad(webView: UIWebView) {
print("Strat Loading")
}
func webViewDidFinishLoad(webView: UIWebView) {
print("Finish Loading")
}
func webView(webView: UIWebView, didFailLoadWithError error: NSError?) {
print(error?.localizedDescription)
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
return true
}
override func webViewDidFinishLoad(_ webView: UIWebView) {
super.webViewDidFinishLoad(webView)
print("Finish Loading 2")
}
But nothing seems to be working.
My class definition seems normal: class WebBrowserViewController: KINWebBrowserViewController, NavigationProtocol {
But no matter what I do - override func or not, no logger or print statement I put in these functions seems to be executed.
What am I doing wrong? I am trying to create an event that will listen to when all of the web content is loaded so I can stop having a loading spinner appear.
EDIT: Adding delegate information:
When I have tried to add a delegate to viewDidLoad():
webView.delegate = self
I have gotten the following error:
Ambiguous reference to member 'webView(_:decidePolicyFor:decisionHandler:)'
This is my viewDidLoad() without the delegate setting:
override func viewDidLoad() {
super.viewDidLoad()
pointsNavigationItem = addPointsButtonToNavigation()
self.showsURLInNavigationBar = false
self.showsPageTitleInNavigationBar = false;
self.tintColor = UIColor.navText
self.barTintColor = .navBackground
}
I'm not sure that KinWebBrowser does delegation the same way?
Added the KINWebBrowserDelegate to the class definition and added the following methods:
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didFailToLoad URL: URL!, error: Error!) {
print("DEBUG 5")
}
func webBrowserViewControllerWillDismiss(_ viewController: KINWebBrowserViewController!) {
print("DEBUG 1")
}
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didStartLoading URL: URL!) {
print("DEBUG 2")
}
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didFinishLoading URL: URL!) {
print("DEBUG 3")
}
func didChangeValue<Value>(for keyPath: KeyPath<WebBrowserViewController, Value>) {
print("DEBUG 4")
}
None of the debugs are getting called in the log.
Those are the UIWebView delegate methods, however from the KINWebBrowser page you should probably be using the KINWebBrowserDelegate protocol
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didStartLoadingURL:(NSURL *)URL;
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didFinishLoadingURL:(NSURL *)URL;
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didFailToLoadURL:(NSURL *)URL withError:(NSError *)error;

webViewDidFinishLoad() does not appear to be running in Swift

I have a view controller for a webview, setup through KINWebBrowser.
I've tried several ways to get these methods to execute:
func webViewDidStartLoad(webView: UIWebView) {
print("Strat Loading")
}
func webViewDidFinishLoad(webView: UIWebView) {
print("Finish Loading")
}
func webView(webView: UIWebView, didFailLoadWithError error: NSError?) {
print(error?.localizedDescription)
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
return true
}
override func webViewDidFinishLoad(_ webView: UIWebView) {
super.webViewDidFinishLoad(webView)
print("Finish Loading 2")
}
But nothing seems to be working.
My class definition seems normal: class WebBrowserViewController: KINWebBrowserViewController, NavigationProtocol {
But no matter what I do - override func or not, no logger or print statement I put in these functions seems to be executed.
What am I doing wrong? I am trying to create an event that will listen to when all of the web content is loaded so I can stop having a loading spinner appear.
EDIT: Adding delegate information:
When I have tried to add a delegate to viewDidLoad():
webView.delegate = self
I have gotten the following error:
Ambiguous reference to member 'webView(_:decidePolicyFor:decisionHandler:)'
This is my viewDidLoad() without the delegate setting:
override func viewDidLoad() {
super.viewDidLoad()
pointsNavigationItem = addPointsButtonToNavigation()
self.showsURLInNavigationBar = false
self.showsPageTitleInNavigationBar = false;
self.tintColor = UIColor.navText
self.barTintColor = .navBackground
}
I'm not sure that KinWebBrowser does delegation the same way?
Added the KINWebBrowserDelegate to the class definition and added the following methods:
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didFailToLoad URL: URL!, error: Error!) {
print("DEBUG 5")
}
func webBrowserViewControllerWillDismiss(_ viewController: KINWebBrowserViewController!) {
print("DEBUG 1")
}
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didStartLoading URL: URL!) {
print("DEBUG 2")
}
func webBrowser(_ webBrowser: KINWebBrowserViewController!, didFinishLoading URL: URL!) {
print("DEBUG 3")
}
func didChangeValue<Value>(for keyPath: KeyPath<WebBrowserViewController, Value>) {
print("DEBUG 4")
}
None of the debugs are getting called in the log.
Those are the UIWebView delegate methods, however from the KINWebBrowser page you should probably be using the KINWebBrowserDelegate protocol
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didStartLoadingURL:(NSURL *)URL;
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didFinishLoadingURL:(NSURL *)URL;
(void)webBrowser:(KINWebBrowserViewController *)webBrowser didFailToLoadURL:(NSURL *)URL withError:(NSError *)error;

Add A Callback To A Prebuilt Asynchronous Function Swift iOS

I'm messing around with pdfs at the moment. I'm attempting to load a PDF into the system and write out the same PDF to gain an understandings of the the whole procedure.
The problem I've got it is that I'm having to load the pdf from the web and because the WebViewUI.loadRequest is asynchronous, it isn't completed in time.
override func viewDidLoad() {
super.viewDidLoad()
let filePath = getDocumentsDirectory().stringByAppendingPathComponent("output.pdf")
let url : NSURL! = NSURL(string: "http://www.nhs.uk/NHSEngland/Healthcosts/Documents/2014/HC5(T)%20June%202014.pdf")
loadTemplate(url, completion: {(webView: UIWebView) -> Void in
print("callback started")
let pdf = self.toPDF(webView)
do {
pdf!.writeToFile(filePath, atomically: true)
} catch {
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}
print("callback started")
})
print("Finished viewDidLoad")
}
func loadTemplate(url: NSURL, completion: (webView: UIWebView) -> Void) {
print("Start loadTemplate")
// do some crunching to create the SketchAnimation instance...
let webView = UIWebView(frame: CGRectMake(20, 100, 300, 40))
webView.loadRequest(NSURLRequest(URL: url))
self.view.addSubview(webView)
// invoke the completion callback
completion(webView: webView)
print("finished loadTemplate")
}
How do I add a callback to the loadRequest instead of loadTemplate?
You don't, exactly. You'd set up your view controller to be the web view's delegate and implement the webViewDidFinishLoad method. In that method you'd check to make sure the load that finished is the one you were after, and if so, then you'd invoked the code you want to run when the load is complete.
Here is a basic example of how to set that up:
//
// ViewController.swift
//
import UIKit
class ViewController: UIViewController, UIWebViewDelegate {
#IBOutlet var webView: UIWebView!
var url = NSURL(string: "http://google.com")
override func viewDidLoad() {
super.viewDidLoad()
//load initial URL
let req = NSURLRequest(URL : url!)
webView.delegate = self
webView.loadRequest(req)
}
func webViewDidStartLoad(webView : UIWebView) {
print("AA")
}
func webViewDidFinishLoad(webView : UIWebView) {
print("BB")
}
}

Activity indicator not stopping when webview has finished loading

Anyone have an idea why my activity indicator is not stopping when the webview has finished loading? Web view is delegated, UIActivityIndicatorView start animating etc, is in my code... ?
This is the relevant code:
import UIKit
class ViewController: UIViewController, UIWebViewDelegate {
#IBOutlet weak var webview: UIWebView!
#IBOutlet weak var activity: UIActivityIndicatorView!
var url = "http://apple.com"
func loadURL () {
let requestURL = NSURL(string: url)
let request = NSURLRequest(URL: requestURL!)
webview.loadRequest(request)
}
override func viewDidLoad() {
super.viewDidLoad()
loadURL()
// Do any additional setup after loading the view, typically from a nib.
webview.delegate = self;
}
func webviewDidStartLoad(_ : UIWebView){
activity.startAnimating()
NSLog("The webview is starting to load")
}
func webviewDidFinishLoad(_ : UIWebView){
activity.stopAnimating()
activity.hidden=true;
NSLog("The webview is done loading")
}
Thanks!
Mind the functions names! It's webView..., camel case :)
Are you getting "The webview is done loading"?. If not, probably the request it's taking a very long time to complete it.
However, you should implement func webView(webView: UIWebView, didFailLoadWithError error: NSError?) to detect if there's any error and stop the activity.
Actually, the correct answer is a probably combination of DAN's and iGongora's, plus a possible third reason.
The immediate problem is that even in a happy path scenario (where the webview finishes successfully) is that it should be the following (pay attention to the case of the function names webViewDidStartLoad and webViewDidFinishLoad
func webViewDidStartLoad(webView: UIWebView) {
activity.startAnimating()
NSLog("The webview is starting to load")
}
func webViewDidFinishLoad(webView: UIWebView) {
activity.stopAnimating()
activity.hidden=true;
NSLog("The webview is done loading")
}
Additionally, if the UIWebView fails, you should also stop the spinner.
func webView(webView: UIWebView!, didFailLoadWithError error: NSError!) {
activity.stopAnimating()
activity.hidden=true;
print("Webview fail with error \(error)");
}
One other possible problem would be if you have not set the delegate properly. You can do it in the viewDidLoad, or in Interface Builder.
override func viewDidLoad() {
super.viewDidLoad()
...
self.webView.delegate = self;
...
}
Run this in main queue so this will stop other wise not the code for this swift 4 and swift 3 is
Dispatchqueue.main.async{
activity.stopAnimating()
}

Resources