XCTest failing even after fulfilling expectation - ios

I have a task aggregator which aggregates and executes tasks in a synchronized manner. It's thread safe and all that. Here's what it looks like
class DaddysMagicalTaskAggregator {
private let tasks: ThreadSafeValueContainer<[KewlTask]>
private let cancelled: ThreadSafeValueContainer<Bool>
private var completion: ((Result<Bool, Error>) -> Void)?
private var alreadyCompleted: Result<Bool, Error>?
private var getTasks: [KewlTask] {
return tasks.value ?? []
}
private var isCancelled: Bool {
return cancelled.value ?? true
}
init(queue: DispatchQueue = DispatchQueue(label: "DaddysMagicalTaskAggregator")) {
self.tasks = ThreadSafeValueContainer(value: [], queue: queue)
self.cancelled = ThreadSafeValueContainer(value: false, queue: queue)
}
/// Add a task to the list of tasks
func addTask(_ task: KewlTask) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + [task]
return self
}
/// Add tasks to the list of tasks
func addTasks(_ tasks: [KewlTask]) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + tasks
return self
}
/// Start executing tasks
#discardableResult
func run() -> DaddysMagicalTaskAggregator {
guard !isCancelled else {
return self
}
guard !getTasks.isEmpty else {
alreadyCompleted = .success(true)
completion?(.success(true))
return self
}
var currentTasks = getTasks
let taskToExecute = currentTasks.removeFirst()
self.tasks.value = currentTasks
taskToExecute.execute { (result) in
switch result {
case .success:
self.run()
case.failure(let error):
self.taskFailed(with: error)
}
}
return self
}
private func taskFailed(with error: Error) {
tasks.value = []
alreadyCompleted = .failure(error)
completion?(.failure(error))
completion = nil
}
/// Add a completion block which executes after all the tasks have executed or upon failing a task.
func onCompletion(_ completion: #escaping (Result<Bool, Error>) -> Void) {
if let result = alreadyCompleted {
completion(result)
} else {
self.completion = completion
}
}
/// Cancel all tasks
func cancelAllTasks(with error: Error) {
cancelled.value = true
taskFailed(with: error)
}
}
public class KewlTask {
private let closure: ((KewlTask) -> Void)
private var completion: ((Result<Bool, Error>) -> Void)?
public init(_ closure: #escaping (KewlTask) -> Void) {
self.closure = closure
}
public func execute(_ completion: #escaping (Result<Bool, Error>) -> Void) {
self.completion = completion
closure(self)
}
/// Task succeeded
func succeeded() {
completion?(.success(true))
}
/// Task failed with given error
func failed(with error: Error) {
completion?(.failure(error))
}
/// Take action based on the result received
func processResult(_ result: Result<Bool, Error>) {
switch result {
case .success:
succeeded()
case .failure(let error):
failed(with: error)
}
}
}
public class ThreadSafeContainer {
fileprivate let queue: DispatchQueue
public init(queue: DispatchQueue) {
self.queue = queue
}
}
public class ThreadSafeValueContainer<T>: ThreadSafeContainer {
private var _value: T
public init(value: T, queue: DispatchQueue) {
self._value = value
super.init(queue: queue)
}
public var value: T? {
get {
return queue.sync { [weak self] in
self?._value
}
}
set(newValue) {
queue.sync { [weak self] in
guard let newValue = newValue else { return }
self?._value = newValue
}
}
}
}
It works as expected. However, when I write a test to make sure it deallocates, the test keeps failing even after the expectation is fulfilled.
Please look at the test code below
import XCTest
class AggregatorTests: XCTestCase {
func testTaskAggregatorShouldDeallocatateUponSuccess() {
class TaskContainer {
let aggregator: CustomKewlAggregator
init(aggregator: CustomKewlAggregator = CustomKewlAggregator()) {
self.aggregator = aggregator
}
}
class CustomKewlAggregator: DaddysMagicalTaskAggregator {
var willDeinit: (() -> Void)?
deinit {
willDeinit?()
}
}
let myExpecation = self.expectation(description: "Task Aggregator should deallocate")
var container: TaskContainer? = TaskContainer()
let t1 = KewlTask { t in
t.succeeded()
}
let t2 = KewlTask {
$0.succeeded()
}
container?.aggregator.willDeinit = {
myExpecation.fulfill()
}
container?.aggregator
.addTasks([t1, t2])
.run()
.onCompletion { (result) in
container = nil
}
waitForExpectations(timeout: 4, handler: nil)
}
}
I've added breakpoints and everything to ensure the expectation fulfillment code does execute.
It doesn't seem to be an xCode issue since I've tested it on XCode 11.7, 12.1, 12.2, and 12.4.
Any idea what's going on here? To me, it looks like a bug in XCTests.

Related

How do you handle a session using WalletConnect?

Purpose: To log in to Web3.0 using metamask in iOS, and to save login sessions to maintain repeated logins.
Problem: The current login is successful, but I don't know where to handle the session.
Source:
https://github.com/maurovz/Glaip <- I use this API
Among those who have implemented WalletConnect in the Swift environment, if anyone knows how to get a session, I'm desperately asking for your help.
// This is Button to login Metamask. action call a function 'glaip.loginUser'
WalletButtonView(
title: "MetaMask",
action: {
glaip.loginUser(type: .MetaMask) { result in
switch result {
case .success(let user):
print("walletAddress:\(user.wallet.address)")
`walletInfo`.walletAddress = user.wallet.address
`walletInfo`.setNFTInfo(user.wallet.address)
case .failure(let error):
print(error)
}
}
},
iconImage: Image("MetaMaskIcon")
)
// This is loginUser function.
public func loginUser(type: WalletType, completion: #escaping (Result<User, Error>) -> Void) {
walletLogin(wallet: type, completion: { [weak self] result in
switch result {
case let .success(user):
DispatchQueue.main.async {
self?.userState = .loggedIn(user)
}
self?.currentWallet = type
completion(.success(user))
case let .failure(error):
completion(.failure(error))
}
})
}
public protocol WalletService {
func connect(wallet: WalletType, completion: #escaping (Result<WalletDetails, Error>) -> Void)
func sign(wallet: WalletType, message: String, completion: #escaping (Result<String, Error>) -> Void)
}
public struct WalletDetails {
public let address: String
public let chainId: Int
public init(address: String, chainId: Int) {
self.address = address
self.chainId = chainId
}
}
public final class WalletLinkService: WalletService {
private let title: String
private let description: String
private var walletConnect: WalletConnect!
private var onDidConnect: ((WalletDetails) -> Void)?
public init(title: String, description: String) {
self.title = title
self.description = description
setWalletConnect()
}
public func connect(wallet: WalletType, completion: #escaping (Result<WalletDetails, Error>) -> Void) {
openAppToConnect(wallet: wallet, getDeepLink(wallet: wallet), delay: 1)
// Temp fix to avoid threading issue with async await
let lock = NSLock()
onDidConnect = { walletInfo in
lock.lock()
defer { lock.unlock() }
completion(.success(walletInfo))
}
}
public func disconnect() {
do {
guard let session = walletConnect.session else { return }
try walletConnect.client.disconnect(from: session)
} catch {
print("error disconnecting")
}
}
public func sign(wallet: WalletType, message: String, completion: #escaping (Result<String, Error>) -> Void) {
openAppToConnect(wallet: wallet, getDeepLink(wallet: .MetaMask), delay: 3)
walletConnect.sign(message: message, completion: completion)
}
private func setWalletConnect() {
walletConnect = WalletConnect(delegate: self)
walletConnect.reconnectIfNeeded()
}
private func openAppToConnect(wallet: WalletType, _ url: String, delay: CGFloat = 0) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if let url = URL(string: self.getDeepLink(wallet: wallet)), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
private func getDeepLink(wallet: WalletType) -> String {
let connectionUrl = walletConnect.connect(title: title, description: description)
let encodeURL = connectionUrl.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""
let end = encodeURL.replacingOccurrences(of: "=", with: "%3D").replacingOccurrences(of: "&", with: "%26")
return "\(wallet.rawValue)\(end)"
}
}
// MARK: - WalletConnectDelegate
extension WalletLinkService: WalletConnectDelegate {
func didUpdate() {
}
func failedToConnect() {
}
func didConnect() {
guard
let session = walletConnect.session,
let walletInfo = session.walletInfo,
let walletAddress = walletInfo.accounts.first
else { return }
onDidConnect?(WalletDetails(address: walletAddress, chainId: walletInfo.chainId))
}
func didDisconnect() {
}
}

Limiting the number of fetch requests in URLSession with SwiftUI?

I'm using an async image loader to fetch images from a URLRequest, and I'm trying to wrap my code inside of an Operation so I can use .maxConcurrentOperationCount for an OperationQueue, because I'm supposed to limit the number of downloads to 3 at a time.
I've overriden the Operation class to try and support async downloads, however, I'm not able to achieve this, and I think it's because my downloading function is inside of a Task group.
The error i get is as follows:
Invalid conversion from 'async' function of type '(URL?, URLResponse?,
(any Error)?) async throws -> Void' to synchronous function type
'(URL?, URLResponse?, (any Error)?) -> Void'
Here are the code snippets:
for the overriden Operation class:
class DownloadOperation: Operation {
private var task: URLSessionDataTask!
init(session: URLSession, downloadTaskURL: URLRequest, completionHandler: ((URL?, URLResponse?, Error?) -> Void)?) {
super.init()
// use weak self to prevent retain cycle
task = session.dataTask(with: downloadTaskURL, completionHandler: { [weak self] (URLRequest, response, error) in
/*
set the operation state to finished once
the download task is completed or have error
*/
self?.state = .finished
})
}
enum OperationState : Int {
case ready
case executing
case finished
}
private var state : OperationState = .ready {
willSet {
self.willChangeValue(forKey: "isExecuting")
self.willChangeValue(forKey: "isFinished")
}
didSet {
self.didChangeValue(forKey: "isExecuting")
self.didChangeValue(forKey: "isFinished")
}
}
override var isReady: Bool { return state == .ready }
override var isExecuting: Bool { return state == .executing }
override var isFinished: Bool { return state == .finished }
override func start() {
/*
if the operation or queue got cancelled even
before the operation has started, set the
operation state to finished and return
*/
if(self.isCancelled) {
state = .finished
return
}
// set the state to executing
state = .executing
print("downloading")
// start the downloading
self.task.resume()
}
override func cancel() {
super.cancel()
// cancel the downloading
self.task.cancel()
}
}
and here is me trying to use it inside of a task in the loader function:
public func loadImage(_ urlRequest: URLRequest) async throws -> UIImage {
if let status = images[urlRequest]{
switch status{
case .fetched(let image):
return image
case .inProgress(let task):
return try await task.value
case .failure(let error):
self.hasError = true
self.error = error as? InternetError
}
}
let task: Task<UIImage, Error> = Task {
do {
let imageQueue = OperationQueue()
imageQueue.maxConcurrentOperationCount = 3
let operation = DownloadOperation(session: URLSession.shared, downloadTaskURL: urlRequest, completionHandler: {_, response ,_ in
let (imageData, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw InternetError.invalidServerResponse
}
guard let image = UIImage(data: imageData) else {
throw InternetError.noInternet
}
})
imageQueue.addOperation(operation)
// return image
}
catch {
self.hasError = true
images[urlRequest] = .failure(error)
print("error caught in Loader")
let image = UIImage(systemName: "wifi.exclamationmark")!
return image
}
}
do{
images[urlRequest] = .inProgress(task)
var image = try await task.value
if let imageFromCache = imageCache.object(forKey: urlRequest as AnyObject) as? UIImage {
image = imageFromCache
return image
}
images[urlRequest] = .fetched(image)
//storing image in cache
imageCache.setObject(image, forKey: urlRequest as AnyObject)
return image
}
}
}
I would appreciate any help about this! Thank you!!
There are several issues:
You are creating a new operation queue every time you call loadImage, rendering the maxConcurrentOperationCount moot. E.g., if you quickly request five images, you will end up with five operation queues, each with one operation on them, and they will run concurrently, with none of the five queues exceeding their respective maxConcurrentOperationCount.
You must remove the local variable declaration of the operation queue from the function, and make it a property.
DownloadOperation is starting a dataTask but not calling the completion handler. Also, when you create the DownloadOperation you are suppling a completion handler in which you are starting yet another download operation. If you are going to use an Operation to encapsulate the download, you should not have any URLSession code in the completion handler. Use the parameters returned.
The asynchronous operation is not thread-safe. One must synchronize the access to this shared state variable.
Thus, perhaps:
var images: [URLRequest: ImageRequest] = [:]
let queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
return queue
}()
let session: URLSession = .shared
public func loadImage(_ request: URLRequest) async throws -> UIImage {
switch images[request] {
case .fetched(let image):
return image
case .inProgress(let task):
return try await task.value
case .failure(let error):
throw error
case nil:
let task: Task<UIImage, Error> = Task {
try await withCheckedThrowingContinuation { continuation in
let operation = ImageRequestOperation(session: session, request: request) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .failure(let error):
self?.images[request] = .failure(error)
continuation.resume(throwing: error)
case .success(let image):
self?.images[request] = .fetched(image)
continuation.resume(returning: image)
}
}
}
queue.addOperation(operation)
}
}
images[request] = .inProgress(task)
return try await task.value
}
}
Where the above, the async-await code, uses the following operation:
class ImageRequestOperation: DataRequestOperation {
init(session: URLSession, request: URLRequest, completionHandler: #escaping (Result<UIImage, Error>) -> Void) {
super.init(session: session, request: request) { result in
switch result {
case .failure(let error):
DispatchQueue.main.async { completionHandler(.failure(error)) }
case .success(let data):
guard let image = UIImage(data: data) else {
DispatchQueue.main.async { completionHandler(.failure(URLError(.badServerResponse))) }
return
}
DispatchQueue.main.async { completionHandler(.success(image)) }
}
}
}
}
The the above abstracts the image-related part, above, from the network-related stuff, below. Thus:
class DataRequestOperation: AsynchronousOperation {
private var task: URLSessionDataTask!
init(session: URLSession, request: URLRequest, completionHandler: #escaping (Result<Data, Error>) -> Void) {
super.init()
task = session.dataTask(with: request) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
completionHandler(.failure(error ?? URLError(.badServerResponse)))
return
}
completionHandler(.success(data))
self.finish()
}
}
override func main() {
task.resume()
}
override func cancel() {
super.cancel()
task.cancel()
}
}
And the above inherits from an AsynchronousOperation that abstracts all of your asynchronous operation stuff, below, from the substance of what the operation does, above. Thus:
/// AsynchronousOperation
///
/// Encapsulate the basic asynchronous operation logic in its own class, to avoid cluttering
/// your concrete implementations with a ton of boilerplate code.
class AsynchronousOperation: Operation {
enum OperationState: Int {
case ready
case executing
case finished
}
#Atomic var state: OperationState = .ready {
willSet {
willChangeValue(forKey: #keyPath(isExecuting))
willChangeValue(forKey: #keyPath(isFinished))
}
didSet {
didChangeValue(forKey: #keyPath(isFinished))
didChangeValue(forKey: #keyPath(isExecuting))
}
}
override var isReady: Bool { state == .ready && super.isReady }
override var isExecuting: Bool { state == .executing }
override var isFinished: Bool { state == .finished }
override var isAsynchronous: Bool { true }
override func start() {
if isCancelled {
state = .finished
return
}
state = .executing
main()
}
/// Subclasses should override this method, but *not* call this `super` rendition.
override func main() {
assertionFailure("The `main` method should be overridden in concrete subclasses of this abstract class.")
}
func finish() {
state = .finished
}
}
And, note, that I addressed the lack of thread-safe access to the state using this property wrapper:
/// Atomic
///
/// Property wrapper providing atomic interface.
///
/// - Note: It is advised to use this with value types only. If you use reference types, the object could theoretically be mutated beyone the knowledge of this property wrapper, losing atomic behavior.
#propertyWrapper
struct Atomic<T> {
var _wrappedValue: T
let lock = NSLock()
var wrappedValue: T {
get { synchronized { _wrappedValue } }
set { synchronized { _wrappedValue = newValue } }
}
init(wrappedValue: T) {
_wrappedValue = wrappedValue
}
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try block()
}
}
This yields asynchronous behaviors with max concurrency count of 3. E.g., here I download 10 images, then another 10, and then another 20:

Network Extension didn't called

I am trying to connect VPN using OpenVPNAdapter but the PacketTunnelProvider isn't called from the controller. What am i missing here?
Controller.swift
import NetworkExtension
var providerManager: NETunnelProviderManager!
var provider = PacketTunnelProvider()
override func viewDidLoad() {
super.viewDidLoad()
self.loadProviderManager {
self.configureVPN(response: self.arrResponse[0], serverAddress: self.arrResponse[0].iP, username: "vpn", password: "vpn")
}
}
func loadProviderManager(completion:#escaping () -> Void) {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if error == nil {
self.providerManager = managers?.first ?? NETunnelProviderManager()
completion()
}
}
}
func configureVPN(response:Response,serverAddress: String, username: String, password: String) {
let data = Data(base64Encoded:response.openVPNConfigDataBase64, options: .ignoreUnknownCharacters)
print(data!)
let decodedString = String(data: data!, encoding: .utf8)!
print(decodedString)
self.providerManager?.loadFromPreferences { error in
if error == nil {
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.username = username
tunnelProtocol.serverAddress = serverAddress
tunnelProtocol.providerBundleIdentifier = "***.*****.********.***********.********"
tunnelProtocol.providerConfiguration = ["ovpn": data!, "username": username, "password": password]
tunnelProtocol.disconnectOnSleep = false
self.providerManager.protocolConfiguration = tunnelProtocol
self.providerManager.localizedDescription = "SMVPN"
self.providerManager.isEnabled = true
self.providerManager.saveToPreferences(completionHandler: { (error) in
if error == nil {
self.providerManager.loadFromPreferences(completionHandler: { (error) in
if error == nil {
self.provider.startTunnel(options: nil) { error in //this called here not in network extension
if error != nil {
print(error!)
}else {
}
}
}else {
print(error!.localizedDescription)
}
})
}
})
}
}
}
Project Entitlement
PacketTunnelProvider.swift
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider: NEPacketTunnelProvider {
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var vpnReachability = OpenVPNReachability()
var configuration: OpenVPNConfiguration!
var properties: OpenVPNConfigurationEvaluation!
var UDPSession: NWUDPSession!
var TCPConnection: NWTCPConnection!
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
override func startTunnel(options: [String : NSObject]?, completionHandler: #escaping (Error?) -> Void) {
// Add code here to start the process of connecting the tunnel.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else { return }
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
do {
properties = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
configuration.tunPersist = true
if !properties.autologin {
if let username: String = providerConfiguration["username"] as? String, let password: String = providerConfiguration["password"] as? String {
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
}
vpnReachability.startTracking { [weak self] status in
guard status != .notReachable else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
vpnAdapter.connect(using: self)
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: #escaping () -> Void) {
// Add code here to start the process of stopping the tunnel.
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
// Add code here to handle the message.
if let handler = completionHandler {
handler(messageData)
}
}
override func sleep(completionHandler: #escaping () -> Void) {
// Add code here to get ready to sleep.
completionHandler()
}
override func wake() {
// Add code here to wake up.
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (Error?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow as? Error : nil)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: #escaping (OpenVPNAdapterPacketFlow?) -> Void) {
setTunnelNetworkSettings(networkSettings) { (error) in
completionHandler(error == nil ? self.packetFlow : nil)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
return
}
NSLog("Error: \(error.localizedDescription)")
NSLog("Connection Info: \(vpnAdapter.connectionInformation.debugDescription)")
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
NSLog("Log: \(logMessage)")
}
}
extension PacketTunnelProvider: OpenVPNAdapterPacketFlow {
func readPackets(completionHandler: #escaping ([Data], [NSNumber]) -> Void) {
packetFlow.readPackets(completionHandler: completionHandler)
}
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
return packetFlow.writePackets(packets, withProtocols: protocols)
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
Extension Entitlement
self.provider.startTunnel(options: nil) { error in // This is called here, not in network extension
This method is called from controller but didn't get called in network extension.
I posted all my code, so if I missed something, then please let me know. Any help is appreciated.
I found this question but I haven't figured it out yet.
PacketTunnelProvider network extension not called Swift 3
You haven't mentioned anything about how you are testing network extension. You should attach a Network extension process before running your project for debugging. Then only network extension methods will trigger for debug.
Finally i found my mistake it's i tried to called network extension but it will never called. so i called NETunnelProviderManager directly and it will work.
self.providerManager.loadFromPreferences(completionHandler: { (error) in
if error == nil {
do {
try self.providerManager.connection.startVPNTunnel()
} catch let error {
print(error.localizedDescription)
}
}else {
print(error!.localizedDescription)
}
})

How can I synchronize closures?

How can I synchronize closures?
I have this code:
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
for _ in 0...10 {
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters, completion: { (result) in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
})
}
completion?()
}
In my code, completion?() will call, not when all requests will ended
And I need call completion?() when all requests will ended. Ho can I do it?
Since the currently accepted answer isn't correct, here is a version that properly uses a DispatchGroup.
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
let dispatchGroup = DispatchGroup()
for _ in 0...10 {
dispatchGroup.enter()
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters) { result in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion?()
}
}
An easy way to do this is just to count completed calls.
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
let numCalls = 11;
var completedCalls = 0;
for _ in 0..<numCalls {
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters, completion: { (result) in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
completedCalls += 1
if completedCalls == numCalls {
completion?()
}
})
}
}
Your completion callback runs when each request ends. Making each of your completion callbacks update a value in its enclosing scope allows you to keep track of how many requests have ended. When all the requests you expect have ended, you call completion?().

How to run synchronically two functions with async operations on iOS using Swift

lets propose this scenario
a method with async network operations
func asyncMethodA() -> String?
{
result : String?
Alamofire.manager.request(.POST, "https://www.apiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200)
{
//DO SOME HEAVY LIFTING
}
}
return result //string
}
another method with async network operations
func asyncMethodB() -> String?
{
result : String?
Alamofire.manager.request(.POST, "https://www.yetanotherapiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200)
{
//DO SOME HEAVY LIFTING
}
}
return result //string
}
a method in which i shall call those methods A and B, to do some operations
func displayResult
{
1) let a = asyncMethodA()
2) let b = asyncMethodB()
3) println(a + b) //some chaotic stuff might happen :(
}
so the question is how i could make that (2) waits for (1) to run, and (3) waits for (2) and so on (that 1,2 and 3 run syncronised)?
(i know that one answer is to chain asyncMethodA and displayResult into asyncMethodB, but want to know if there is some other method)
thank you!.
func anAsyncMethod(resultHandler: (result: AnyObject) -> Void) {
...
}
func anotherAsyncMethod(resultHandler: (result: AnyObject) -> Void) {
...
}
let operationQueue = NSOperationQueue()
func performWithCompletionHandler(completion: (AnyObject?, AnyObject?) -> Void) {
var resultOfOperation1: AnyObject?
var resultOfOperation2: AnyObject?
let operation1 = NSBlockOperation {
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
self.anAsyncMethod {
result in
resultOfOperation1 = result
dispatch_group_leave(dispatchGroup)
}
// wait until anAsyncMethod is completed
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
}
let operation2 = NSBlockOperation {
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
self.anotherAsyncMethod {
result in
resultOfOperation2 = result
dispatch_group_leave(dispatchGroup)
}
// wait until anotherAsyncMethod is completed
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
}
let completionOperation = NSBlockOperation {
// send all results to completion callback
completion(resultOfOperation1, resultOfOperation2)
}
// configuring interoperation dependencies
operation2.addDependency(operation1)
completionOperation.addDependency(operation2)
operationQueue.addOperations([operation1, operation2, completionOperation], waitUntilFinished: false)
}
Thanks Yimin for the code above. I've updated it to the latest Swift syntax so just posting to be helpful:
func anAsyncMethod(resultHandler: (_ result: AnyObject) -> Void) {
...
}
func anotherAsyncMethod(resultHandler: (_ result: AnyObject) -> Void) {
...
}
func performWithCompletionHandler(completion: #escaping (AnyObject?, AnyObject?) -> Void) {
let operationQueue = OperationQueue()
var resultOfOperation1: AnyObject?
var resultOfOperation2: AnyObject?
let operation1 = BlockOperation {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
self.anAsyncMethod {
result in
resultOfOperation1 = result
dispatchGroup.leave()
}
// wait until anAsyncMethod is completed
dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
let operation2 = BlockOperation {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
self.anotherAsyncMethod {
result in
resultOfOperation2 = result
dispatchGroup.leave()
}
// wait until anotherAsyncMethod is completed
dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
let completionOperation = BlockOperation {
// send all results to completion callback
completion(resultOfOperation1, resultOfOperation2)
}
// configuring interoperation dependencies
operation2.addDependency(operation1)
completionOperation.addDependency(operation2)
operationQueue.addOperations([operation1, operation2, completionOperation], waitUntilFinished: false)
}
With the below, you can launch both async methods at the same time and do your heavy lifting after whichever one finishes last.
var methodAFinished = false
var methodBFinished = false
func asyncMethodA() -> String?
{
Alamofire.manager.request(.POST, "https://www.apiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200) {
methodAFinished = true
doStuff()
}
}
return result //string
}
The guts of asyncMethodB would be methodBFinished = true; doStuff()
func doStuff() {
if methodAFinished && methodBFinished {
// do crazy stuff
}
}

Resources