How to call multiple Api based on the previous Api success - ios

I wanted to make multiple API call in the same screen, but when one api fails other api should not be called? The below code is working fine. but what I need is , how can I refactor this in a more simpler way?
ApplicationService.requestAppEndPointUrl { success, error in
if success {
ApplicationService.appLinkDownload { success, error in
if success{
ApplicationService.requestApplicationSession { success, error in
if success {
ApplicationService.validateSdk { success, error in
if success {
ApplicationService.requestApplicationDetails { success, error in
if success{
print("Success")
}
else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}

If ApplicationService is a class/struct that you can modify, you could convert the synchronous function calls with completion handler to asynchronous function calls, using Swift 5.5 concurrency. The code would look like:
do {
try await ApplicationService.requestAppEndPointUrl()
try await ApplicationService.appLinkDownload()
try await ApplicationService.requestApplicationSession()
try await ApplicationService.validateSdk()
try await ApplicationService.requestApplicationDetails()
} catch {
self.showErrorAlert(error)
}
Then, the 1st error would break the call chain, would be cought and shown.
If you had to use the synchronous ApplicationService functions, you could convert them to asynchronous functions by using async wrappers as shown in the link cited above.

for this, you need to use OperationQueue & ** DispatchGroup** where you can make your API calls in BlockOperation and one operation depends on another, and their dispatch group helps you to hold the API calls.
func apiCall() {
let dispatchGroup = DispatchGroup()
let queue = OperationQueue()
let operation1 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestAppEndPointUrl {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestAppEndPointUrl()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
let operation2 = BlockOperation {
dispatchGroup.enter()
ApplicationService.appLinkDownload {
print("ApplicationService.appLinkDownload()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation2.addDependency(operation1)
let operation3 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationSession {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationSession()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation3.addDependency(operation2)
let operation4 = BlockOperation {
dispatchGroup.enter()
ApplicationService.validateSdk {
print("ApplicationService.validateSdk()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation4.addDependency(operation3)
let operation5 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationDetails {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationDetails()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation5.addDependency(operation4)
queue.addOperations([operation1, operation2, operation3, operation4, operation5], waitUntilFinished: true)
}
I know code is a little bit nasty.
and if you are using swift 5.5 then use Async await mentioned by #Reinhard Männer

Thats a pretty question. So if you don't want to pass data from one api call to the other the solution is pretty easy.
All you have to do is to make a Service Manager that will manage all the service calls.
You can make an enum based on the services you want to call
enum ServiceEnum {
case requestAppEndPointUrl
case appLinkDownload
case requestApplicationSession
case validateSdk
case requestApplicationDetails
}
enum ServiceState {
case notStarted
case inProgress
case finished
case failed
}
Then you can make a class of the service
class Service {
var service: ServiceEnum
var serviceState: ServiceState = .notStarted
init(service: ServiceEnum) {
self.service = service
}
func updateState(serviceState: ServiceState) {
self.serviceState = serviceState
}
}
After that you will need to make the Service Manager
class ServiceManager {
private var services = [Services]()
private var indexForService: Int {
didSet {
self.startServices()
}
}
init() {
services.append(.requestAppEndPointUrl)
services.append(.appLinkDownload)
services.append(.requestApplicationSession)
services.append(.validateSdk)
services.append(.requestApplicationDetails)
indexForService = 0
}
func startServices() {
// Check if the process has ended
guard indexForService < services.count else {
// If you want you can call an event here.
// All the services have been completed
return
}
// Check that we have services to call
guard services.count > 0 else {
return
}
handleService(service: services[indexForService])
}
func haveAllFinished() -> Bool {
return services.fist(where: { $0.serviceState == .failed || $0.serviceState == .notStarted || $0.serviceState == .inProgress }) == nil
}
func handleService(service: Service) -> Bool {
switch(service) {
case .requestAppEndPointUrl:
ApplicationService.requestAppEndPointUrl { success, error in
let serviceState = success ? .finished : .failed
service.updateState(serviceState: serviceState)
if success {
self.indexForService += 1
} else {
self.showErrorAlert(error)
}
}
// Do the same for other services
}
}
}

Related

EXC_BAD_ACCESS when initializing Dictionary of CurrentValueSubject in Swift

I am trying to create a class that executes data loading once and returns the data to all callers of the method while the data was loading to not perform the data loading for the same item (identifier) more than once. The issue I am having is that it seems to crash on the first initialization of CurrentValueSubject for an identifier. This only happens if the downloadStuff returns an Error I have no idea what's wrong. Here is a reproduction of the issue.
Class that does the synchronization:
class FetchSynchronizer<T, ItemIdentifier: Hashable> {
typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)
enum FetchCondition {
// executes data fetching only once
case executeFetchOnlyOnce
// re-executes fetch if request failed
case retryOnlyIfFailure
// always executes fetch even if response is cached
case noDataCache
// custom condition
case custom((CustomParams) -> Bool)
}
struct LoadingState<T> {
let result: Result<T, Error>
let isLoading: Bool
init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
self.result = result ?? .failure(NoResultsError())
self.isLoading = isLoading
}
}
private var cancellables = Set<AnyCancellable>()
private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
func startLoading(identifier: ItemIdentifier,
fetchCondition: FetchCondition = .executeFetchOnlyOnce,
loaderMethod: #escaping () async -> Result<T, Error>) async -> Result<T, Error> {
// initialize loading tracker for identifier on first execution
var isFirstExecution = false
if isLoading[identifier] == nil {
print("----0")
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
isFirstExecution = true
}
guard let currentIsLoading = isLoading[identifier] else {
assertionFailure("Should never be nil because it's set above")
return .failure(NoResultsError())
}
if currentIsLoading.value.isLoading {
// loading in progress, wait for finish and call pending callbacks
return await withCheckedContinuation { continuation in
currentIsLoading.filter { !$0.isLoading }.sink { currentIsLoading in
continuation.resume(returning: currentIsLoading.result)
}.store(in: &cancellables)
}
} else {
// no fetching in progress, check if it can be executed
let shouldFetchData: Bool
switch fetchCondition {
case .executeFetchOnlyOnce:
// first execution -> fetch data
shouldFetchData = isFirstExecution
case .retryOnlyIfFailure:
// no cached data -> fetch data
switch currentIsLoading.value.result {
case .success:
shouldFetchData = false
case .failure:
shouldFetchData = true
}
case .noDataCache:
// always fetch
shouldFetchData = true
case .custom(let completion):
shouldFetchData = completion((isFirstLoad: isFirstExecution,
result: currentIsLoading.value.result))
}
if shouldFetchData {
currentIsLoading.send(LoadingState(isLoading: true))
// fetch data
return await withCheckedContinuation { continuation in
Task {
// execute loader method
let result = await loaderMethod()
let state = LoadingState(result: result,
isLoading: false)
currentIsLoading.send(state)
continuation.resume(returning: result)
}
}
} else {
// use existing data
return currentIsLoading.value.result
}
}
}
}
Example usage:
class Executer {
let fetchSynchronizer = FetchSynchronizer<Data?, String>()
func downloadStuff() async -> Result<Data?, Error> {
await fetchSynchronizer.startLoading(identifier: "1") {
return await withCheckedContinuation { continuation in
sleep(UInt32.random(in: 1...3))
print("-------request")
continuation.resume(returning: .failure(NSError() as Error))
}
}
}
init() {
start()
}
func start() {
Task {
await downloadStuff()
print("-----3")
}
DispatchQueue.global(qos: .utility).async {
Task {
await self.downloadStuff()
print("-----2")
}
}
DispatchQueue.global(qos: .background).async {
Task {
await self.downloadStuff()
print("-----1")
}
}
}
}
Start the execution:
Executer()
Crashes at
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
Any guidance would be appreciated.
Swift Dictionary is not thread-safe.
You need to make sure it is being accessed from only one thread (i.e queue) or using locks.
EDIT - another solution suggested by #Bogdan the question writer is to make the class an actor class which the concurrency safety is taken care of by the compiler!
By dispatching to a global queue, you increase the chance that two threads will try and write into the dictionary “at the same time” which probably causes the crash
Take a look at these examples.
How to implement a Thread Safe HashTable (PhoneBook) Data Structure in Swift?
https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift

Make a network call every 10 seconds with RxSwift

I am completely new to RxSwift. I managed to load my table view but now I would like to make a call every 10 seconds.
I was reading here that I should probably use Observable<Int>.interval(10, scheduler: MainScheduler.instance), I tried without much success.
class MarketService: MarketServiceProtocol {
func fetchMarkets() -> Observable <[Market]> {
return Observable.create { observer -> Disposable in
RestManager.shared.makeRequest(withEndPoint: "market/v2/get-summary?region=US" , withHttpMethod: .get) { result in
if let error = result.error {
observer.onError(error)
return
}
guard let response = result.response,
200 ... 299 ~= response.httpStatusCode else {
return
}
guard let data = result.data else {
return
}
do {
let decodedData = try JSONDecoder().decode(MarketResult.self, from: data)
observer.onNext(decodedData.marketSummaryAndSparkResponse.markets)
} catch {
observer.onError(error)
}
}
return Disposables.create { }
}
}
}
then I call in my view controller:
viewModel.fetchMarketViewModels().observe(on: MainScheduler.instance).bind(to: tableView.rx.items(cellIdentifier: HomeTableViewCell.cellIdentifier)) {
index, viewModel, cell in
guard let cell = cell as? HomeTableViewCell else { return }
cell.setupData(viewModel: viewModel)
}.disposed(by: self.disposableBag)
There are a couple of problems with your Observable.create closure. You have to make sure that something is sent to the observer in every path, otherwise the Observable will call the function and then not emit anything and you will not know why.
Also, you want to minimize the amount of logic being performed in the closure passed to create. You are doing way too much in there.
So let's simplify the code in the create closure as much as possible first:
extension RestManager {
func rx_makeRequest(withEndPoint endPoint: String, withHttpMethod method: HttpMethod) -> Observable<(response: MyHTTPURLResponse, data: Data)> {
Observable.create { observer in
self.makeRequest(withEndPoint: endPoint, withHttpMethod: method) { result in
if let response = result.response, let data = result.data {
observer.onNext((response, data))
observer.onCompleted()
}
else {
observer.onError(result.error ?? RxError.unknown)
}
}
return Disposables.create() // is there some way of canceling a request? If so, it should be done here.
}
}
}
This does the bare minimum. Just wraps the underlying callback and nothing else. Now your fetchMarkets call is much simpler:
class MarketService: MarketServiceProtocol {
func fetchMarkets() -> Observable <[Market]> {
return RestManager.shared.rx_makeRequest(withEndPoint: "market/v2/get-summary?region=US", withHttpMethod: .get)
.do(onNext: { result in
guard 200...299 ~= result.response.httpStatusCode
else { throw URLError.httpRequestFailed(response: result.response, data: result.data) }
})
.map { try JSONDecoder().decode(MarketResult.self, from: $0.data).marketSummaryAndSparkResponse.markets }
}
}
Now to the meat of your question. How to make the network call every 10 seconds... Just wrap your network call in a flatMap like this:
Observable<Int>.interval(.seconds(10), scheduler: MainScheduler.instance)
.flatMapLatest { _ in
viewModel.fetchMarketViewModels()
}
.observe(on: MainScheduler.instance)
.bind(to: tableView.rx.items(cellIdentifier: HomeTableViewCell.cellIdentifier)) { index, viewModel, cell in
guard let cell = cell as? HomeTableViewCell else { return }
cell.setupData(viewModel: viewModel)
}
.disposed(by: self.disposableBag)
Learn more about flatMap and its variants from this article.

iOS - SwiftUI - Navigate to the next screen AFTER async actions performed

I am pretty new to SwiftUI and with DispatchGroups and DispatchQueues.
I would like to create a Button which processes some server requests and then use the returned data with a CoreML model to predict some score. Once, the score is predicted, then the app can navigate to the next screen
Here is the sequence of actions which need to be done before moving to the next screen
// exemple of sequence of actions
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.name = self.names[self.selectedCompanyIndex]
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex])
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex])
group.leave()
}
group.notify(queue: .main) {
print("done")
}
//function for fetching tweets
func fetchTweets(company: String) {
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
}
}
//function for making predictions via the coreML model
func makePrediction(with tweets: [TextClassifier1Input], type: String) {
do {
let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
var sentimentScore = 0
for pred in predictions {
if pred.label == "pos" {
sentimentScore += 1
} else if pred.label == "neg" {
sentimentScore -= 1
} else {
print("something sent wrong: --> ", pred.label)
}
}
if type == "#" {
arobaseScore = sentimentScore
} else if type == "#" {
hashScore = sentimentScore
}
} catch {
print("There was an error with the ML model: --> ", error)
}
}
The problem is the navigation is executed on the button click whereas I want the previous actions to be done before.
Can anyone let me know how should I use DispatchGroups and DispatchQueue to run my code in the correct sequence
Thanks in advance for your help
welcome to Stack Overflow.
You are not waiting for swifter.searchTweet to complete before doing the update.
The first step is to add a completion handler to fetchTweets. The completion handler is a way of reporting when fetchTweets has finished the networking.
//function for fetching tweets
func fetchTweets(company: String, completion: #escaping () -> Void) {
// ^^^^^^^^^^ Added completion handler
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
completion() // <-- Call completion on success
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
completion() // <-- Call completion on failure
}
}
After that is done, then you can enter and leave the group when the networking is done. The code in notify will then be called. It's in that code block where you need to update the UI.
let group = DispatchGroup()
self.name = self.names[self.selectedCompanyIndex]
group.enter()
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.enter()
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.notify(queue: .main) {
print("done")
// BEGIN UPDATING THE UI
// NOTE: Here is where you update the UI.
// END UPDATING THE UI
}
for those using programmatic navigation, trying to navigate before a call to the backend is done will cause all sorts of problems where the view wants to still stay until the call is over and your navigation logic wants to continue

how to manage a several asynchronous task before doing some action?

I am beginner in programming. I actually have my own answer of this questions and the app worked as I am expected, but I am not sure if this is the correct way to to this.
This check out action will be triggered after the user click chechoutButton. but before before this chechoutButton.isEnabled , I have to make sure 3 parameters are available (not nil). before doing this check out action, I need 3 parameters :
get user's coordinate from GPS.
get user's location address from Google Place
API
Get current date time from server for verification.
method to get user location address from Google Place API will be triggered only if I get the coordinate from GPS, and as we know, fetching data from the internet (to take date and time) also takes time, it should be done asynchronously.
how do I manage this checkoutButton only enabled if those 3 parameters are not nil ? Is there a better way according to apple guideline to do this
the simplified code are below
class CheckoutTVC: UITableViewController {
#IBOutlet weak var checkOutButton: DesignableButton!
var checkinAndCheckoutData : [String:Any]? // from MainMenuVC
var dateTimeNowFromServer : String?
var userLocationAddress : String?
let locationManager = LocationManager()
var coordinateUser : Coordinate? {
didSet {
getLocationAddress()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// initial state
checkOutButton.alpha = 0.4
checkOutButton.isEnabled = false
getDateTimeFromServer()
getCoordinate()
}
#IBAction func CheckoutButtonDidPressed(_ sender: Any) {
}
}
extension CheckoutTVC {
func getDateTimeFromServer() {
activityIndicator.startAnimating()
NetworkingService.getDateTimeFromServer { (result) in
switch result {
case .failure(let error) :
self.activityIndicator.stopAnimating()
// show alert
case .success(let timeFromServer) :
let stringDateTimeServer = timeFromServer as! String
self.dateTimeNowFromServer = stringDateTimeServer
self.activityIndicator.stopAnimating()
}
}
}
func getCoordinate() {
locationManager.getPermission()
locationManager.didGetLocation = { [weak self] userCoordinate in
self?.coordinateUser = userCoordinate
self?.activateCheckOutButton()
}
}
func getLocationAddress() {
guard let coordinateTheUser = coordinateUser else {return}
let latlng = "\(coordinateTheUser.latitude),\(coordinateTheUser.longitude)"
let request = URLRequest(url: url!)
Alamofire.request(request).responseJSON { (response) in
switch response.result {
case .failure(let error) :// show alert
case .success(let value) :
let json = JSON(value)
let locationOfUser = json["results"][0]["formatted_address"].string
self.userLocationAddress = locationOfUser
self.locationAddressLabel.text = locationOfUser
self.activateNextStepButton()
}
}
}
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
}
I manage this by using this method, but I don't know if this is the correct way or not
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
You can use DispatchGroup to know when all of your asynchronous calls are complete.
func notifyMeAfter3Calls() {
let dispatch = DispatchGroup()
dispatch.enter()
API.call1() { (data1)
API.call2(data1) { (data2)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
}
dispatch.enter()
API.call3() { (data)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
dispatch.notify(queue: DispatchQueue.main) {
finished?(dispatchSuccess)
}
}
You must have an equal amount of enter() and leave() calls. Once all of the leave() calls are made, the code in DispatchGroupd.notify will be called.

Wait for asynchronous request result

I would like somehow to asynchronously validate the pin in ABPadLockScreen since pins are not saved on the device. I'm using Alamofire for http requests along with PromiseKit to have promises.
I have tried to use AwaitKit but the problem is that i get into a deadlock.
I have also tried to use semaphore as well, but the result is the same. Since i can't change the ABPadLock method to accommodate something like a completion handler i need some solution, it doesn't matter if it blocks the main thread, just that it works.
Alamofire request method:
public func loginAsync(pinCode: String?, apiPath: String?) -> Promise<LoginResult>{
return Promise { fullfil, reject in
let params = [
"Pin": pinCode!
]
Alamofire.request(.POST, "\(baseUrl!)/\(apiPath!)", parameters: params).responseObject{(response: Response<LoginResult, NSError>) in
let serverResponse = response.response
if serverResponse!.statusCode != 200 {
reject(NSError(domain: "http", code: serverResponse!.statusCode, userInfo: nil))
}
if let loginResult = response.result.value {
fullfil(loginResult)
}
}
}
}
ABPadLockScreen pin validation method:
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
let pinCode = pin!
let defaults = NSUserDefaults.standardUserDefaults()
let serverUrl = defaults.stringForKey(Util.serverUrlKey)
let service = AirpharmService(baseUrl: serverUrl)
service.loginAsync(pinCode, apiPath: "sw/airpharm/login").then { loginResult -> Void in
if loginResult.code == HTTPStatusCode.OK {
AirpharmService.id = loginResult.result!.id
}
}
return false // how do i get the result of above async method here?
}
With semaphore:
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
var loginResult: LoginResult?
let defaults = NSUserDefaults.standardUserDefaults()
let baseUrl = defaults.stringForKey(Util.serverUrlKey)
let service = AirpharmService(baseUrl: baseUrl)
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResultRaw -> Void in
loginResult = loginResultRaw
dispatch_semaphore_signal(semaphore)//after a suggestion from Josip B.
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
return loginResult != nil // rudimentary check for now
}
EDIT:
After a suggestion from Josip B. i added semaphore signal in then, but it still doesn't work
AirpharmService is a class that contains a static property called id, and the Alamofire request method.
ABPadLockScreen pin validation is done on main thread in a ViewController
SOLVED EDIT:
Thanks to everyone for being so patient with me and my, not so good, knowledge of swift and iOS. There are a lot of good answers here and in the end i just went with, in my opinion, simplest solution. I listened to Losiowaty-s suggestion; implemented a spinner and manually dismissed the lock screen when i get the response from the server. I've used a SwiftSpinner. The final solution looked like this:
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
let defaults = NSUserDefaults.standardUserDefaults()
let baseUrl = defaults.stringForKey(Util.serverUrlKey)
let service = AirpharmService(baseUrl: baseUrl)
SwiftSpinner.show("Logging in. Please wait...")
service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResult -> Void in
if loginResult.code == HTTPStatusCode.OK {
SwiftSpinner.hide()
AirpharmService.id = loginResult.result!.id
self.unlockWasSuccessfulForPadLockScreenViewController(padLockScreenViewController)
} else if loginResult.code == HTTPStatusCode.Unauthorized {
let toast = JLToast.makeText("Invalid pin, please try again", duration: 5)
toast.show()
SwiftSpinner.hide()
} else {
let toast = JLToast.makeText("\(loginResult.code) sent from server. Please try again.", duration: 5)
toast.show()
SwiftSpinner.hide()
}
}.error { error in
let toast = JLToast.makeText("\((error as NSError).code) sent from server. Please try again.", duration: 5)
toast.show()
SwiftSpinner.hide()
}
return false
}
It's great that a lot of people tried to help you make your asynchronous call synchronous. Personally I agree with #OOPer and his comment that you should redesign your code, especially after looking through ABPadLockScreen code. It seems they don't support asynchronous pin verification, which is a shame. Also it seems from their github repo that the original author has abandoned the project, for the time being at least.
I'd attempt to solve your issue like this :
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
let pinCode = pin!
let defaults = NSUserDefaults.standardUserDefaults()
let serverUrl = defaults.stringForKey(Util.serverUrlKey)
let service = AirpharmService(baseUrl: serverUrl)
service.loginAsync(pinCode, apiPath: "sw/airpharm/login").then { loginResult -> Void in
if loginResult.code == HTTPStatusCode.OK {
AirpharmService.id = loginResult.result!.id
self.handleLoginOk()
} else {
self.handleLoginFailed()
}
}
// disable interaction on padlock screen
// indicate to user that an async operation is going on, show a spinner
return false // always return false here
}
func handleLoginOk() {
// dismiss the ABPadlockScreenViewController manually
}
func handleLoginFailed() {
// dismiss the spinner indicating the async operation
// restore user interaction to padlock screen
}
With this approach your users will know that something is going on (the spinner, you can use for example SVProgressHUD as a drop-in solution) and that the app didn't hang. It is quite important, ux-wise, as users with poor connection could get frustrated thinking the app hanged and close it.
There is a potential problem though - if the padlock screen shows some kind of "wrong pin" message when you return false from the delegate method, it could be visible to the user creating some confusion. Now this can be tackled by making/positioning the spinner so that it obscures the message, though this is a very crude and unelegant solution. On the other hand, maybe it can be customised enough so that no message gets shown, and you'd display your own alert after server side verification.
Let me know what you think about this!
... it doesn't matter if it blocks the main thread... but the problem is that i get into a deadlock.
One problem could be it is blocking the main thread with dispatch_semaphore_wait, so the Alamofire response never get a chance to run on the main thread and you're deadlocking.
The solution to this could be create another queue on which the Alamofire completion handler is dispatched.
For example:
If you making a request like this:
Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts").validate().responseData() { response in
print(response.result.value)
}
You can modify this call to dispatch completion handler in your defined queue like this:
let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT)
let request = Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts", parameters: .None).validate()
request.response(queue: queue, responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments)) { response in
print(response.result.value)
}
A simplified version for test.
//MARK: Lock Screen Delegate
func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
print("Validating Pin \(pin)")
let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT)
let semaphore = dispatch_semaphore_create(0)
let request = Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts", parameters: .None).validate()
request.response(queue: queue, responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments)) { response in
print(response.result.value)
//isPinValid = ???
dispatch_semaphore_signal(semaphore);
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
return thePin == pin
//return isPinValid
}
Try this:
add dispatch_group:
static let serviceGroup = dispatch_group_create();
Then after calling the function, wait for this group:
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
var loginResult: LoginResult?
let defaults = NSUserDefaults.standardUserDefaults()
let baseUrl = defaults.stringForKey(Util.serverUrlKey)
let service = AirpharmService(baseUrl: baseUrl)
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResultRaw -> Void in
loginResult = loginResultRaw
}
dispatch_group_wait(yourClass.serviceGroup, DISPATCH_TIME_FOREVER);
return loginResult != nil // rudimentary check for now
}
And release the group after the function returns an answer:
public func loginAsync(pinCode: String?, apiPath: String?) -> Promise<LoginResult>{
return Promise { fullfil, reject in
let params = [
"Pin": pinCode!
]
Alamofire.request(.POST, "\(baseUrl!)/\(apiPath!)", parameters: params).responseObject{(response: Response<LoginResult, NSError>) in
let serverResponse = response.response
if serverResponse!.statusCode != 200 {
reject(NSError(domain: "http", code: serverResponse!.statusCode, userInfo: nil))
}
if let loginResult = response.result.value {
fullfil(loginResult)
}
dispatch_group_leave(yourClass.serviceGroup)
}
}
}
Based on the comments we exchanged, it sounds like the endless wait when you tried using a semaphore is because the semaphore signal is never being sent. Let's try to simplify this down to the minimum code needed to test:
public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
var success = false
let defaults = NSUserDefaults.standardUserDefaults()
let baseUrl = defaults.stringForKey(Util.serverUrlKey)
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
let params = ["Pin": pin]
Alamofire.request(.POST, "\(baseUrl!)/sw/airpharm/login", parameters: params).responseObject {
(response: Response<LoginResult, NSError>) in
if let loginResult = response.result.value where loginResult.code == HTTPStatusCode.OK {
AirpharmService.id = loginResult.result!.id
success = true
}
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
return success
}
This should either:
crash because you are force unwrapping several variables (e.g.baseUrl!, loginResult.result!.id, etc. and one of them is nil
return true if you got a valid LoginResult
return false if you didn't get a valid LoginResult
But theoretically, it shouldn't deadlock.
I've tried to make the ABPadLockScreen support asynchronous pin verification.
I've modified ABPadLockScreenViewController. Added a new ABPadLockScreenViewControllerDelegate protocol method shouldValidatePinManuallyForPadLockScreenViewController:.
/**
Call when pin validation is needed manually
Call processUnlock method to validate manually if return true from this method
*/
- (BOOL)shouldValidatePinManuallyForPadLockScreenViewController:(ABPadLockScreenViewController *)padLockScreenViewController;
Added a new instance method processUnlock
- (void)processUnlock {
if ([self isPinValid:self.currentPin]) {
[self unlockScreen];
} else {
[self processFailure];
}
}
Modified the processPin method
- (void)processPin {
if ([self.lockScreenDelegate respondsToSelector:#selector(shouldValidatePinManuallyForPadLockScreenViewController:)]) {
if ([self.lockScreenDelegate shouldValidatePinManuallyForPadLockScreenViewController:self]) {
return;
}
}
[self processUnlock];
}
Now in your viewController implement shouldValidatePinManuallyForPadLockScreenViewController
func shouldValidatePinManuallyForPadLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!) -> Bool {
print("Requesting server...")
Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts").validate().responseJSON() { response in
//isPinValid = ???
print("Request complete")
padLockScreenViewController.processUnlock()
}
return true
}
Made a demo project at https://github.com/rishi420/ABPadLockScreen
See the swift demo example.
I think semaphore can help. Here is a usage example:
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(#selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(tasks))]) {
tasks = [#[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:#"#unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
This is a function comes from AFNetworking. The method getTasksWithCompletionHandler is a method of NSURLSession which will
Asynchronously calls a completion callback with all data, upload, and download tasks in a session.
Semaphore_wait will ensure that tasks has be assigned with proper value. This way you can get the asynchronously request result.

Resources