Creating a closure inline - ios

In WKWebView we have a method evaluateJavaScript. I have overridden this method to add custom code.
override open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Swift.Void)? = nil)
{
let b:Bool? = true
let error: Error? = NSError(domain: "com.My.Tests", code: 1000, userInfo: nil) as Error
let completion = (b, error) -> Void
evaluateJavaScript(javaScriptString, completionHandler: completion)
}
How to pass the completion? It is throwing an error.

That's not a real closure.
The signature expects something like
let completion = { (b, error) in print(b, error) }
The declared b and error variables are useless because b and error are returned from the method.

override open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Swift.Void)? = nil)
{
let b:Bool? = true
let error: Error? = NSError(domain: "com.My.Tests", code: 1000, userInfo: nil) as Error
evaluateJavaScript(javaScriptString) { (result, error) in
if error != nil {
completionHandler?(nil, error)
} else {
completionHandler?(result, nil)
}
}
}

Your completion is not a closure. It should be something like
let completion = { (b: Any?, error: Error?) -> Void in
print("\(error)")
}
I'm presuming that you want your WKWebView to act like some Javascript evaluation failed. If that's the case, you'd either:
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
evaluateJavaScript(javaScriptString) { b, error in
completionHandler?(true, NSError(domain: "com.My.Tests", code: 1000))
}
}
Or, if you didn't really need to evaluate the Javascript, but just look like a failure (e.g. stubbing it in a test):
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
completionHandler?(true, NSError(domain: "com.My.Tests", code: 1000))
}

Related

How can I chain completion handlers if one method also has a return value?

I have 2 methods I need to call, the second method must be executed using the result of the first method and the second method also returns a value.
I have put together a simple playground that demonstrates a simple version of the flow
import UIKit
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
completion("some.access.token")
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
completion(.success((data, response)))
}
})
task.resume()
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
I need to use the token returned by AuthTokenLoader as a header on the request sent by dispatch method in my Networking class.
Networking also returns a task so this request can be cancelled.
As I cannot return from inside the completion block of the AuthTokenLoader load completion, I unsure how to achieve this.
You can create a wrapper for your task and return that instead.
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
Your entire playground would become
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("some.access.token")
}
}
}
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task {
let task = URLSessionTaskWrapper(completion)
loader.load("token") { token in
task.wrapped = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
task.complete(with: .failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
task.complete(with: .success((data, response)))
}
})
task.wrapped?.resume()
}
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
Turns out it was harder to do in Combine than I thought. Like most people, I'm still quite new to this. Would happily accept edits from people who know better :)
The general principle is that, instead of taking a completion block, your functions should return a Publisher, that you can then choose to do things with, and chain together.
So your token loader can look like this:
protocol TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error>
}
Instead of taking a completion block, you now return a publisher which will send you a string on success, or an error if something goes wrong.
And your implementation, well I wasn't sure what you were planning on doing in there but here's an example of sorts:
class AuthTokenLoader: TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error> {
print("was called")
// Do async stuff to create your token, ending in a result
let result = Result<String, Error>.success("some.access.token")
return result.publisher.eraseToAnyPublisher()
}
}
Your client can look like this:
protocol Client {
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error>
}
Now this is the complicated bit. What you want to do is take the publisher from your token loader, and when it gives a result, make your URL request, and then make another publisher from that URL request. URLSession can give you a publisher for a data task, and there is a flatMap operator which is supposed to allow you to turn the results of one publisher into a new publisher, but you get stuck in the weeds of the type system so the code is uglier than it ought to be:
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error> {
return loader.load("someKey")
.flatMap {
token -> AnyPublisher<Data, Error> in
var finalRequest = request
finalRequest.setValue(token, forHTTPHeaderField: "x-token")
return URLSession.shared.dataTaskPublisher(for: finalRequest)
.map { $0.data }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
You'd use this code like so:
let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
let sub = client.dispatch(request)
.sink(receiveCompletion: {
switch $0 {
case .finished: print("Done")
case .failure(let error): print("Error :\(error)")
}
}, receiveValue: { data in
print("Data: \(data)")
})
sub is an AnyCancellable, so if it is deallocated or you call cancel on it, this passes back up the chain and it will cancel the URL task for you.
If you want to do things with the data then there are operators for mapping or decoding or whatever which makes the whole thing very nice to work with.

How to unit test async code which has DispatchQueue.main.async in it

I've written below swift code
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
DispatchQueue.main.async {
guard let error = error else {
completion(result, nil)
return
}
completion(result, error)
}
}
)}
and while unit test it I'm facing issue.
This works perfectly on local but while generating teamcity build it get failed.
please let me know how can I unit test above piece of code?
Let me first suggest eliminating the extra code around calling your completion handler. The guard statement is redundant.
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
DispatchQueue.main.async {
completion(result, error)
}
}
)}
Next, I would suggest passing in the queue that you want to run the completion on ... but with a default value of DispatchQueue.main so that your application call sites don't change but in your test code you can pass in something else.
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void, onQueue queue: DispatchQueue = DispatchQueue.main) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
queue.async {
completion(result, error)
}
}
)}

Correct memory management in closures

I have method with closure.
It works like this
func ping() -> pingProtocol {
let factory = pingFactory()
let ping = factory.getPing() // returns pingProtocol conforming instance, for example "PingService"
return ping
}
func performPing(success: #escaping () -> (), fail: #escaping () -> ()) {
ping().status { (ready, response) in
if ready {
success()
} else {
fail()
}
}
}
Realisation of ping is abstraction over Alamofire.
class PingService {
func status(completion: #escaping (Bool, ServiceResponse) -> Void) {
doSimpleRequest(endPoint: "/api/get_state/", param: [:], completion: completion)
}
func doSimpleRequest(endPoint: String, param: [String: Any], completion: #escaping (Bool, ServiceResponse) -> Void) {
APIManager.shared.postRequest(mode: APIMode.Service, endPoint: endPoint, parameters: param) { [weak self] (response, error) in
guard let strongSelf = self else {
return
}
guard let response = response as? [String: Any] else { return }
let serviceResponse = ServiceResponse(dict: response)
if error != nil {
completion(false, serviceResponse)
return
}
completion(true, serviceResponse)
}
}
}
The problem is that with weak self guard in self is always triggers. If I use unowned, I get crash. If I dont use weak or unowned it works fine.
The ping method itself lives in viewmodel of controller, and controller stays on screen when completion block is needed to be executed, so the problem is in deallocation of ping instance, not in viewmodel thats holds it.
If I use capture list I dont get desired behavior, but If I do I suspect that I get memory leak.
What is best practice here ?

DispatchGroup returning multiple times

Below code which I am using to make concurrent API call. somehow this method returning multiple times. I have tested without DispatchGroup, It is working as expected. Help me to find why it is calling multiple times.
My Code Snippet :
func makeConcurrentCallForUpdating(_ parent: Parent,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void)
let fetchGroup = DispatchGroup()
let queue = DispatchQueue.global(qos: .default)
let endPoints = [.email, .others ]
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
let enumType = endPoints[index]
switch enumType {
case .email:
updateEmail(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
case .others:
update(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
default:
break
}
}
fetchGroup.notify(queue: queue) {
if self.endPoints.count > 0 {
completionBlock(false, error)
} else {
self.saveUpdated(parent, completionBlock: completionBlock)
}
}
}
#MARK: EMAIL CALL
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
fetchGroup.enter()
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}
You need to enter() all dispatched tasks before any task in the group will leave().
Move your fetchGroup.enter() before DispatchQueue.concurrentPerform(...
let endPoints = [.email, .others ]
endPoints.forEach {_ in fetchGroup.enter()} //<- add this line
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
And remove fetchGroup.enter() in each task:
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
//fetchGroup.enter() //<- remove this line, in your `update(...)` as well
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}
Please try.

Preventing URLSession redirect in Swift

I need to fetch a redirecting URL but prevent redirection in Swift. From other posts and Apple docs I understand I must implement the delegate method URLSession(session:, task:, willPerformHTTPRedirection response:, request:, completionHandler:) and return nil via the completion closure. But I can't find examples in swift, nor figure out the right way to do it. The code below reproduces my issue in playground: the delegate does not seem to get executed.
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
class MySession: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
// trying to follow instructions at https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionTaskDelegate_protocol/index.html#//apple_ref/occ/intfm/NSURLSessionTaskDelegate/URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
// to prevent redirection -- DOES NOT SEEM TO GET CALLED
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
println("in URLSession delegate") // NEVER PRINTS
completionHandler(nil) // NO EFFECT
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, success: (response: String!) -> Void) {
var session = NSURLSession.sharedSession()
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html><head><title>Object moved</title>", options: .RegularExpressionSearch) != nil {
println("success: redirection was prevented") // SHOULD PRINT THIS
} else {
println("failure: redirection went through") // INSTEAD PRINTS THIS
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer") // ex. redirecting link
Please be gentle, I am a newbie. Thank you in advance for any assistance!
UPDATE: With many thanks to #nate I got it to work. The key insight is that in order for the delegate to be called, one must pass the delegate class to the NSURLSession() initializer, rather than using NSURLSession.sharedSession(). Passing nil as the delegate yields the customary behavior (with redirection). Here is working version of the code:
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
class MySession: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
// to prevent redirection
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(nil)
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool, success: (response: String!) -> Void) {
var myDelegate: MySession? = nil
if noRedirect {
myDelegate = MySession()
}
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url, noRedirect: true) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html>\n<head><title>Bitly</title>", options: .RegularExpressionSearch) != nil {
println("success: redirection was prevented")
} else {
println("failure: redirection went through")
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer")
You have two things standing in your way with your current implementation.
You never set the delegate property on the NSURLSession instance that you're using to make the request. Without the delegate property set, your delegate methods won't ever be called. Instead of getting NSURLSession.sharedSession(), look at the NSURLSession(configuration:delegate:delegateQueue:) initializer. The first and last parameters can be NSURLSessionConfiguration.defaultSessionConfiguration() and nil, respectively, see below for more about the delegate.
Note that when you use the variant of session.dataTaskWithURL that has a completion handler, delegate methods that handle response and data delivery will be ignored, but authentication and redirection handlers are still used.
You'll have to refactor somewhat to use MySession as a delegate, since you're using class methods to make the request. You need an instance to use as the session's delegate.
I took a short and incomplete route to having the delegate pick up on the redirect with this alternate code—you'll need to refactor as in #3 to make sure you can still call your callback:
class func getDataFromServerWithSuccess(myURL: String, success: (response: String!) -> Void) {
let delegate = MySession()
var session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: delegate, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: myURL)!) {
// ...
}
task.resume()
}
Hope that helps!
I also found the solution helpful, but I am using Swift 4.2 in my current project.
So, here is an adapted shorter version of the solution above, that also works with Swift 4.2 and Xcode 10.
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
class MySession: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
completionHandler(nil)
}
}
func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool) {
let myDelegate: MySession? = noRedirect ? MySession() : nil
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTask(with: URL(string:myURL)!) { (data, response, error) in
// OMITTING ERROR CHECKING FOR BREVITY
if let data = data {
if let dataString = String(bytes: data, encoding: .utf8) {
print(dataString)
if dataString.contains("Bitly") == true {
print("success: redirection was prevented")
} else {
print("failure: redirection went through")
}
}
}
}
loadDataTask.resume()
}
getDataFromServerWithSuccess(myURL: "http://bitly.com/filmenczer", noRedirect: true)
Minor adjustments to the solutions proposed above. This works with Swift 2 in XCode 7.
import UIKit
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(true)
class MySession: NSObject, NSURLSessionDelegate {
// to prevent redirection
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(nil)
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool, success: (response: String!) -> Void) {
var myDelegate: MySession? = nil
if noRedirect {
myDelegate = MySession()
}
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as! String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url, noRedirect: true) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html>\n<head><title>Bitly</title>", options: .RegularExpressionSearch) != nil {
print("success: redirection was prevented")
} else {
print("failure: redirection went through")
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer")

Resources