Swift + iOS: How to call closure from within async call? - ios

I have a web view inside of my app. I'm currently looking at each request and seeing if they are a youtube video before allowing / canceling the request:
extension WebBrowserViewController: WKNavigationDelegate {
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
if (urlVideoType(webView.URL!) != VideoUrl.Unknown && urlVideoType(webView.URL!) != VideoUrl.Youtube) {
...
} else if urlVideoType(webView.URL!) == VideoUrl.Youtube {
// the success and failure part don't work
presentYoutubeVideoIfAvailable(webView.URL!, success: decisionHandler(.Cancel), failure: decisionHandler(.Allow)) <-------------------
} else {
decisionHandler(.Allow)
}
}
}
My presentYoutubeVideoIfAvailable method uses this youtube url parser pod: https://github.com/movielala/YoutubeSourceParserKit
However, that makes an async call and I don't know how to make it call the success and failure methods in my method:
func presentYoutubeVideoIfAvailable(url: NSURL, success: (), failure: ()) {
Youtube.h264videosWithYoutubeURL(url) { [unowned self] (videoInfo, _) -> Void in
switch videoInfo?["url"] as? String {
case .Some(let videoUrlString):
VideoStore.addVideo(url.absoluteString, title: videoInfo?["title"] as? String ?? "Unknown")
success
self.presentVideo(NSURL(string: videoUrlString)!)
case .None:
print("herereee") // gets printed, so I know it's here
failure // doesn't do anything. it should be allowing the request but it isn't <-----------------------------------
self.showError(.YoutubeParsingFail)
}
}
}
I want it so that if the youtube pod returns a url, it should cancel the web view request, and if it fails I want it to continue with the request. How would I do this?

The reason nothing is happening here is that you're not calling the closures. Closures should be called like any other function in swift, like so:
success()
failure()

Related

Write Unit Tests to validate WKWebView load request

I have a WKWebView to loads a basic url request.
extension ViewController: WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate {
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
if url.description.lowercased().range(of: "http://") != nil ||
url.description.lowercased().range(of: "https://") != nil ||
url.description.lowercased().range(of: "mailto:") != nil {
UIApplication.shared.open(url)
}
}
return nil
}
func openSite() {
guard let myUrl = URL(string: "https://www.myurl.com") else { return }
let request = URLRequest(url: myUrl)
webView?.load(request)
self.webView.sizeToFit()
}
Now I want to write a unit test to verify webview correctly load the request. I have followed this approach https://stackoverflow.com/a/63827560/627667 and created a mock navigation action.
func test_AllowsCorrectURL() {
let action = MockNavigationAction()
action.mockedRequest = URLRequest(url: URL(string: "https://www.myurl")!)
let allowExpectation = expectation(description: "Allows action")
viewController.webView(WKWebView(), decidePolicyFor: action) { policy in
XCTAssertEqual(policy, .allow)
allowExpectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
However on this line viewController.webView(WKWebView(), decidePolicyFor: action) I am getting below error.
Cannot call value of non-function type 'WKWebView?'
Swift version 5. How to get rid of this error? Your suggestions highly appreciated.
I'm almost sure that this is related to you adding implementation for another method of WKNavigationDelegate to your vc. It has few of them containing decidePolicyFor parameter label and if you will omit the implementation for the one that you are trying to call in here you won't be able to do so.
The reason of that (to my understanding) is that these are methods marked as optional on the protocol so your class doesn't need to implement them and if compiler won't find implementation of the method that you try to call it will complete with the error.
Since you shared a concrete example, you're basing your solution on check if your vc is having a method with exactly this signature:
extension ViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// ...
}
}

How to mock URLSession.DataTaskPublisher

How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a URLSessionProtocol
protocol URLSessionProtocol {
func loadData(from url: URL) -> URLSession.DataTaskPublisher
}
class Proxy {
private let urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol) {
self.urlSession = urlSession
}
func get(url: URL) -> AnyPublisher<Data, ProxyError> {
// Using urlSession.loadData(from: url)
}
}
This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.
Is it possible to do the same with the Combine Framework?
In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:
let mockPublisher = Just(MockData()).eraseToAnyPublisher()
However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:
Why does Combine's receive(on:) operator swallow errors?
The best way to test your client is to use URLProtocol.
https://developer.apple.com/documentation/foundation/urlprotocol
You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests.
Tests more reliable, faster, and you got the control!
You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...
I hope it'll help you!
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).

How do I get WKWebView.evaluateJavaScript to return data in a function call

I'm working on some WKWebView parsing routines. I'm trying to validate that I've navigated to a page properly by checking it's document.title. I wrote a function to do this work, but I can't seem to figure out how to return the HTML data from the function or do the evaluation in the function and return a BOOL. I know I'm doing an async call here, but not sure how to wait for that call to end and feed the response back from my function call.
Here is my function:
func checkTitle (forWebView webView: WKWebView, title: String) -> String{
webView.evaluateJavaScript("document.title", completionHandler: { (innerHTML, error ) in
let titleString = innerHTML as? String
return (titleString)
})
This throws a compiler error. I've tried to declare the variable outside the call and then assign and return it after, but it tries to execute that before the async call is complete.
you should use a completion handler, something like this:
func checkTitle (forWebView webView: WKWebView, title: String, completion: #escaping (_ titleString: String?) -> Void) {
webView.evaluateJavaScript("document.title", completionHandler: { (innerHTML, error ) in
// Logic here
completion(innerHTML as? String)
})
}

How to use typealias when getting data from server

I am trying to get user data from a server. The application does not have to show any views until the data is loaded.
I read about typealias and I don't understand how to use it.
What I want: when data is loaded, move on to next step. If failed, load data again.
Here's how I declare typealias
typealias onCompleted = () -> ()
typealias onFailed = () -> ()
Here is my request code
func getUserData(_ completed: #escaping onCompleted, failed: #escaping onFailed){
let fullURL = AFUtils.getFullURL(AUTHURL.getUserData)
AFNetworking.requestGETURL(fullURL, params: nil, success: {
(JSONResponse) -> Void in
if let status = JSONResponse["status"].string {
switch status{
case Status.ok:
completed()
break
default:
failed()
break
}
}
})
}
But how could I use this on my view controller when calling getUserData?
Assuming your custom AFNetworking.requestGETURLs completion handler is called on the main queue:
func viewDidLoad() {
super.viewDidLoad()
getUserData({
//do somthing and update ui
}) {
//handle error
}
}
Edit:
How I understand your comment, you actually want to name your completion and error block parameters. If so, change the method to :
func getUserData(completion completed: #escaping onCompleted, error failed: #escaping onFailed){ ... }
and call it like this:
getUserData(completion: {
//do somthing and update ui
}, error: {
//handle error
})

WKWebView in iOS: How can I intercept a click and retrieve the linked content instead?

In a WKWebView, when a user clicks a link that refers to certain file types (e.g. a VCF file for contacts, or an ICS file for calendar events), I'd like to intercept the link, i.e. cancel the navigation, and instead display the content using a specialized view controller.
For example, the CNContactViewController can be used to display contacts, the EKEventViewController can be used to display calendar events.
I can intercept the click by assigning a WKNavigationDelegate and using decidePolicyForNavigationAction:
// Swift 2
extension MyController: WKNavigationDelegate {
func webView(webView: WKWebView, decidePolicyForNavigationAction
navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> ()) {
let url = navigationAction.request.URL!
if url.pathExtension == "ics" {
decisionHandler(WKNavigationActionPolicy.Cancel)
// TODO: download data
// TODO: display with EKEventViewController
} else if url.pathExtension == "vcf" {
decisionHandler(WKNavigationActionPolicy.Cancel)
// TODO: download data
// TODO: display with CNContactViewController
} else {
decisionHandler(WKNavigationActionPolicy.Allow)
}
}
}
But in order to display the files the specialized controllers, I need to download the data from the given url first.
How can I do that?
Since the download requires authentication, the download needs to share the cookies with the WKWebView, or use another technique to share the already authenticated session.
If it helps: I've already got access to the web view's WKProcessPool and WKWebViewConfiguration. To my understanding, the cookies are somehow tied to the WKProcessPool. But I don't know how to use this to download the content, for example with a NSURLSession.
It feels hacky, but I solved this by having the WKWebView execute some javascript that retrieves the content via ajax and returns it to a completionHandler in swift.
Background
The WKWebView supports calling evaluateJavaScript, which passes the javascript's result to a completionHandler:
func evaluateJavaScript(_ javaScriptString: String,
completionHandler completionHandler: ((AnyObject?, NSError?) -> Void)?)
Since there's jQuery on the server side, I used this to send an ajax request like follows. But, of course, this can be done with vanilla javascript as well.
(function(url) {
var result = '';
$.ajax({
type: 'GET',
url: url,
success: function(r) {result = r},
failure: function() {result = null},
async: false
});
return result
})(url)
The url can be passed to javascript with swift's string interpolation.
Extend WKWebView
To easily use this, I've extended the WKWebViewclass.
// Views/WKWebView.swift
import WebKit
extension WKWebView {
func readUrlContent(url: NSURL, completionHandler: (result: String) -> Void) {
self.evaluateJavaScript("(function() { var result = ''; $.ajax({type: 'GET', url: '\(url)', success: function(r) {result = r}, failure: function() {result = null}, async: false }); return result })()", completionHandler: { (response, error) -> Void in
let result = response as! String
completionHandler(result: result)
})
}
}
Usage
From the question's example, this can be called like this:
let url = navigationAction.request.URL!
if url.pathExtension == "ics" {
decisionHandler(WKNavigationActionPolicy.Cancel)
webView.readUrlContent(url) { (result: String) in
print(result)
// TODO: display with EKEventViewController
}
}

Resources