1

Background: In order to make web requests to an API endpoint, I need to scrape a website and retrieve a token every 25-30 seconds. I'm doing this with a WKWebView and injecting some custom JavaScript using WKUserScript to retrieve AJAX response headers containing the token. Please focus on the question specifically and not on this background information - I'm attempting this entirely for my own educational purposes.

Goal

I will have different 'model' classes, or even just other UIViewControllers, that may need to call the shared UIViewController to retrieve this token to make an authenticated request.

Maybe I might abstract this into one "Sdk" class. Regardless, this 'model' SDK class could be instantiated and used by any other ViewController.

More info

I would like to be able to call the UIViewController of the WKWebView and retrieve some data. Unless I re-create it every 25 seconds, I need to run it in the background or share it. I would like to be able to run a UIViewController 'in the background' and receive some information from it once WKWebView has done it's thing.

I know there are multiple ways of communicating with another ViewController including delegation and segueing. However, I'm not sure that these help me keep the view containing the WKWebView existing in the background so I can call it's ViewController and have it re-perform the scrape. Delegation may work for normal code, but what about one that must have the view existing? Would I have to re-create this WKWebView dynamically each time a different model, or view controller, were to try and get this token?

One post suggests utilising ContainerViewControllers. From this, I gather that in the 'master' ViewController (the one containing the other ones), I could place the hidden WKWebView to do it's thing and communicate to the child view controllers that way via delegation.

Another post suggests using AppDelegate and making it a shared service. I'm completely against using a Singleton as it is widely considered an anti-pattern. There must be another way, even if a little more complex, that helps me do what I want without resorting to this 'cheat'.

This post talks about communicating between multiple ViewControllers, but I can't figure out how this would be useful when something needs to stay running and executing things.

How about any other ways to do this? Run something in a background thread with a strong pointer so it doesn't get discarded? I'm using Xcode 9.2, Swift 4, and iOS 11. As I'm very new to iOS programming, any small code examples on this would be appreciated.

Jimbo
  • 25,790
  • 15
  • 86
  • 131
  • if your scraping it, it doesnt neccesarily have to render? which means you could just wrap it in a singleton like NSObject structure that handles all of the refreshing and any controller can access it – Sean Lintern Dec 07 '17 at 16:07
  • Thanks, I put in my message that I'm not looking to use a Singleton. That would be simple, and a last resort. I've added why I don't want to use a **Singleton** there :) Side-question, it would not need to render to perform the scraping etc?? – Jimbo Dec 07 '17 at 16:09
  • So if I understood correctly, you need to make a call to get the token every 30 seconds, and you need that up to date token available for any view controller to use? – Mohamad Bachir Sidani Dec 07 '17 at 16:10
  • then still make a wrapper, as I stated and pass it between classes, it doesnt have to be a singleton, whichever pattern you choose its not going to work out loosely bound, least bound would probably be use a DB to store the last known key, make a wrapper and inject the context, save the key on every successful scrape, fetch the key every time something wants to use it – Sean Lintern Dec 07 '17 at 16:11
  • @MohammadBashirSidani Yep. If it was just a standard HTTP request, I'd put this in a model class and then just use that wherever. Instead, I use a `WKWebView` and this of course needs a `ViewController` etc – Jimbo Dec 07 '17 at 16:11
  • it doesnt 'need' a uiviewcontroller if your never visually seeing it ? – Sean Lintern Dec 07 '17 at 16:12
  • How about defining a timer in the app delegate, that will trigger a notification. Define a method thats called when this notification is trigerred that makes the call, And up on success save the new token inside of an NSDefault string. When you want to fetch it from anywhere, just fetch the value inside of the NSDefault – Mohamad Bachir Sidani Dec 07 '17 at 16:13
  • @SeanLintern88 Thanks for your response - new to iOS dev. So you suggest creating and rendering the `WKWebView` programatically with (it's hidden attribute set to true), storing that in the DB and then pulling from there. Can you do this in a thread? I assumed a `UIViewController` was needed to talk to it. – Jimbo Dec 07 '17 at 16:14
  • 1
    kinda, you dont have to actually add the WKWebview to anything, so you dont need to set anything to hidden, just make a class of NSObject with a var webView: WKWebView, init it and there you have it, it will still work 100%, you will just never render it. the NSobject can be made from anywhere, maybe in your app delegate, and as long as you keep a ref to it, you can inject an NSManagedObject context or as stated simply us UserDefaults to store the key, anything that needs it can pull from the same persistence – Sean Lintern Dec 07 '17 at 16:17
  • @SeanLintern88 Great, thanks! Let me go give this a shot as it's taught me a good few things already. My only confusion is keeping a reference to it in AppDelegate, is this effectively making it a global var? – Jimbo Dec 07 '17 at 16:22
  • Looks like I've hit a stumbling block - **WKWebView must be in the view heirarchy** to work. So I can't just use it in the background :( [link 1](https://stackoverflow.com/questions/31169884/wkwebkit-javascript-execution-when-not-attached-to-a-view-hierarchy), [link 2](https://stackoverflow.com/questions/39074768/wkwebview-is-not-loading-resources-when-not-presented) – Jimbo Dec 07 '17 at 17:18

2 Answers2

3

Unfortunately, WKWebView must be in the view hierarchy to use it. You must have added it as a sub view of an on-screen view controller.

This was fine for me. I added this off-screen so it was not visible. Hidden attribute might have worked as well. Either way you must call addSubview with it to make it work.

There are some other questions and answers here which verify this.

Jimbo
  • 25,790
  • 15
  • 86
  • 131
-1

Here is a way if you don't wish to use a singleton.

1- In the DidFinishlaunchingWithOptions, Make a timer that runs in the background and call a method inside the app delegate Called FetchNewToken.

2- In FetchNewToken, make the call needed and retrieve the new token (you can use alamofire or any 3rd library to make the call easier for you).

Up on successfully retrieving the token, save it in NSUserDefaults under the name upToDateToken

You can access this token anywhere from the application using NSUserDefaults and it will always be up to date.

Mohamad Bachir Sidani
  • 2,077
  • 1
  • 11
  • 17
  • 1
    Thanks! But I'm not looking for the Singleton, it's widely regarded as an anti-pattern (and I do mention this in my question). – Jimbo Dec 07 '17 at 16:07
  • Again thanks Mohammad. Both Singleton and, seemingly, AppDelegate usage provide a global variable which I'm trying to avoid. However, if I can't get anything else working, I'll use it. So for now, leave your answer here and if I can't do anything else I'll use this and mark it as the answer :) – Jimbo Dec 07 '17 at 16:29