ios 10+, Swift 3+ - Cannot dismiss UIAlertController from Singleton instance - ios

I have created an overlay to run while I run an async data grab to the server so that users won't continue pressing buttons in the UI until the data grab is done. I have put the function into a global singleton class and I call it while passing in a bool to say whether or not I want to show or hide. I can get it to show but I cannot get it to hide. Here is the code:
class DataModel {
static let sharedInstance = DataModel()
func accessNetworkData(vc: UIViewController, params: [String:Any], wsURLPath: String, completion: #escaping (_ response: AnyObject) -> ()) {
DataModel.sharedInstance.toggleModalProgess(show: true)
// SHOW THE MODAL HERE ONCE THE DATA IS REQUESTED.
let url = URL(string: wsURLPath)!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
do { request.httpBody = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) } catch let error { print(error.localizedDescription) }
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
DataModel.sharedInstance.toggleModalProgess(show: false)
// NOW SINCE THE NETWORK ACTIVITY IS DONE, HIDE THE UIALERTCONTROLLER
guard error == nil else {
print("WEB SERVICE ERROR <----------------------------<<<<<< " + wsURLPath)
print(error!)
let resp: [String: String] = [ "conn": "failed" ]
DispatchQueue.main.async { completion(resp as NSDictionary) }
return
}
guard let data = data else {
print("WEB SERVICE ERROR <----------------------------<<<<<< " + wsURLPath)
return
}
do {
if let parsedJSON = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print("WEB SERVICE SUCCESS <----------------------------<<<<<< "+wsURLPath+" "+String(describing:params))
if let parsedResponseDict = parsedJSON["d"] as? NSDictionary {
DispatchQueue.main.async {
completion(parsedResponseDict)
}
}else if let parsedResponseArr = parsedJSON["d"] as? NSArray {
DispatchQueue.main.async {
completion(parsedResponseArr)
}
}else {
print("INVALID KEY <----------------------------<<<<<< " + wsURLPath)
DispatchQueue.main.async {
completion(parsedJSON as AnyObject)
}
}
}
} catch let error {
print("Error with JSON Serialization")
print(error.localizedDescription)
}
})
task.resume()
}
HERE IS WHERE I SET UP THE UIALERTCONTROLLER
let modalAlert = UIAlertController(title: "Please Wait...", message: "Loading Data...", preferredStyle: UIAlertControllerStyle.alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
func toggleModalProgess(show: Bool) -> Void {
print("toggleModalProgess: show = " + String(describing: show))
if (show) {
print("let's turn it on")
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
loadingIndicator.startAnimating()
modalAlert.view.addSubview(loadingIndicator)
modalAlert.show()
}else {
print("let's turn it off")
modalAlert.hide()
}
}
private init() { }
}
NOW THE EXTENSION WHERE THE MAGIC HAPPENS
public extension UIAlertController {
func show() {
let win = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
win.rootViewController = vc
win.windowLevel = UIWindowLevelAlert + 1
win.makeKeyAndVisible()
vc.present(self, animated: true, completion: nil)
}
func hide() {
// HERE IS WHERE I NEED TO HIDE IT BUT I AM HAVING ISSUES
}
}

In order to dismiss the UIAlertController (which is a subclass of UIViewController), it should be sufficient to call the dismiss method:
func hide() {
dismiss(animated: true, completion: nil)
}
It works fine in my sample project.

You should do...
self.presentingViewController?.dismiss(animated: true, completion: nil)
Hope it helps

Related

How to show activity indication on the centre of screen when tapping on OK button (using AVCaptureVideoPreviewLayer)?

How to show activity indication on the centre of screen when tapping on OK button ? I am using AVCaptureVideoPreviewLayer object and it show the Camera and when we scan the QR Code then alert box appears, after pressing OK button then it calls API and in this time, I want to appear the UIActivityIndicator.
import UIKit
import AVFoundation
class QRCodeScanner: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
#IBOutlet weak var imgClose: UIImageView!
var video = AVCaptureVideoPreviewLayer()
var strQR = String()
override func viewDidLoad() {
super.viewDidLoad()
//Creating session
let session = AVCaptureSession()
//Capture device
//var captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
let captureDevice = AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera, for: AVMediaType.video, position: .front)
do {
let input = try AVCaptureDeviceInput(device: captureDevice!)
session.addInput(input)
}
catch {
print("Error")
}
let output = AVCaptureMetadataOutput()
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
output.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]
video = AVCaptureVideoPreviewLayer(session: session)
video.frame = view.layer.bounds
view.layer.addSublayer(video)
//Show Image in Camera Mode
let myLayer = CALayer()
let myImage = UIImage(named: "icn-close")?.cgImage
myLayer.frame = CGRect(x: self.view.frame.size.width - CGFloat((myImage?.width)!) - 10, y: 16, width: 32, height: 35)
myLayer.contents = myImage
video.addSublayer(myLayer)
self.view.bringSubview(toFront: self.view)
session.startRunning()
}
#IBAction func btnClose(_ sender: Any)
{
self.navigationController?.popViewController(animated: true)
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection)
{
if metadataObjects.count == 0 {
//qrCodeFrameView?.frame = CGRect.zero
//messageLabel.text = "No QR code is detected"
return
}
// Get the metadata object.
let object = metadataObjects[0] as? AVMetadataMachineReadableCodeObject
let supportedCodeTypes = AVMetadataObject.ObjectType.qr
if supportedCodeTypes == .qr {
// If the found metadata is equal to the QR code metadata (or barcode) then update the status label's text and set the bounds
//let barCodeObject = videoPwreviewLayer?.transformedMetadataObject(for: metadataObj)
//qrCodeFrameView?.frame = barCodeObject!.bounds
let alert = UIAlertController(title: "QR Code", message: object?.stringValue, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Retake", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (nil) in
self.strQR = (object?.stringValue)!
//print("strQR")
activityIndicator.startAnimating()
self.api(strURL: urlQRCode)
}))
present(alert, animated: true, completion: nil)
if object?.stringValue != nil {
//launchApp(decodedURL: metadataObj.stringValue!)
//messageLabel.text = metadataObj.stringValue
}
}
}
func api(strURL: String)
{
//URL
let myURL = URL(string: strURL)
//URL Request
let request = NSMutableURLRequest(url: myURL!)
request.httpMethod = "POST"
//Passing the strOTP text in the dictionary variable
let postString = ["qr_code": strQR]
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let token = "Bearer " + strToken
request.addValue(token, forHTTPHeaderField: "Authorization")
do {
// pass dictionary to nsdata object and set it as request body
request.httpBody = try JSONSerialization.data(withJSONObject: postString, options: .prettyPrinted)
//print("Successfully passed data to server")
} catch let error {
print(error.localizedDescription)
}
let postTask = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
//print(response!)
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print("POST Method :\(json)")
let dict = json as? [String: String]
let status = dict!["status"]
print("Status : \(String(describing: status))")
//Match the status code (received from response through the server
if dict!["status"] == "1"
{
DispatchQueue.main.async {
//Stop Activity Indicator
activityIndicator.stopAnimating()
print("The status received from server is 1. Entered Successfully to OTP Screen")
let thankVC = self.storyboard?.instantiateViewController(withIdentifier: "ThankYouVC") as! ThankYouVC
thankVC.strEmpName = dict!["whom_to_meet"]!
self.navigationController?.pushViewController(thankVC, animated: true)
}
}
else
{
print("The status received from server is 0.")
let whoopsPopupVC = self.storyboard?.instantiateViewController(withIdentifier: "Popup_Whoops") as! Popup_Whoops
whoopsPopupVC.strDetail = "You are not a registered user."
self.navigationController?.present(whoopsPopupVC, animated: true, completion: nil)
}
// handle json...
}
} catch let error {
print(error.localizedDescription)
}
}
postTask.resume()
}
}

sending information to another view controller

I found a barcode scanner project on git hub that I incorporated into my app link. I am using google books API to get information on the books I scanned.
func getBookInfo(isbn: String) {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=isbn13:\(isbn)") else {
print("the url is not valid")
return
}
URLSession.shared.dataTask(with: url, completionHandler: {data, response, error -> Void in
guard error == nil else {
print(response)
print(error!.localizedDescription)
return
}
guard let data = data else {
print("no error but no data")
print(response)
return
}
guard let jsonResult = try? JSONSerialization.jsonObject(with: data, options: []) else {
print("the JSON is not valid")
return
}
if let arrayOfTitles = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.title") as? [String] {
self.BookName.text = "\(arrayOfTitles[0])"
print(self.BookName.text!)
}
if let arrayOfAuthors = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.authors") as? [[String]] {
self.Author.text = "\((arrayOfAuthors[0])[0])"
print(self.Author.text!)
}
if let arrayOfCategories = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.categories") as? [[String]] {
self.Category.text = "\((arrayOfCategories[0])[0])"
print(self.Category.text!)
}
if let arrayOfISBN13 = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.industryIdentifiers.identifier") as? [[String]] {
self.ISBN13.text = "\((arrayOfISBN13[0])[0])"
print(self.ISBN13.text!)
}
if let arrayOfISBN10 = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.industryIdentifiers.identifier") as? [[String]] {
self.ISBN10.text = "\((arrayOfISBN10[0])[1])"
print(self.ISBN10.text!)
}
if let arrayOfFormat = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.printType") as? [String] {
self.CoverType.text = "\(arrayOfFormat[0])"
print(self.CoverType.text!)
}
}).resume()
}
After I scan the books and have received the information, I would like to dismiss the view controller that has the barcode scanner and in the view controller that appears, I would like to display the information of the book I just scanned.
extension MultipleImageViewController: BarcodeScannerCodeDelegate {
func barcodeScanner(_ controller: BarcodeScannerController, didCaptureCode code: String, type: String) {
if code.isEmpty {
let delayTime = DispatchTime.now() + Double(Int64(6 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: delayTime) {
controller.resetWithError()
}
}
else{
getBookInfo(isbn: code)
let delayTime = DispatchTime.now() + Double(Int64(6 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: delayTime) {
controller.dismiss(animated: true, completion: nil)
}
}
}
}
However, when the barcode scanner view controller is dismissed, I have to exit the app and then come back to the app in order for my information to show in the view controller that I want it to. Without leaving the app and coming back the information that I received from the barcode scanner does not display in the desired view controller.
Your UI is not updating because the method viewDidLoad does not get called when that view controller is returned to since it already loaded. Instead, create a delegate for the parent that will get called when you dismiss the child view controller. Sort of like this:
class ParentViewController: UIViewController, BarcodeDelegate {
func presentChildViewController() {
let childViewController = ChildViewController()
childViewController.delegate = self
present(childViewController, animated: true, completion: nil)
}
func dismissedChildViewController(code: String) {
// update UI
}
}
class ChildViewController: UIViewController {
var delegate: BarcodeDelegate?
func dismiss() {
delegate.dismissedChildViewController(code: getCode())
dismiss(animated: true, completion: nil)
}
}
protocol BarcodeDelegate {
func dismissedChildViewController(code: String)
}
It is difficult to really understand what the problem is, so this may or may not be what you're looking for.

Swift Convert string into UIIMAGE

I would like to load an image from an api web service asynchronously into a uitableview with swift for iOS 9. Below is the code from my Playlist controller. Thanks in advance.
import UIKit
class PlaylistViewController: UITableViewController {
var playlists = [[String: String]]()
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "http://xxxxxxx.xxx/api/v1/players/1/playlists?api_key=xxxxxxxxxxxx"
if let url = NSURL(string: urlString) {
if let data = try? NSData(contentsOfURL: url, options: []) {
let json = JSON(data: data)
if json != nil {
parseJSON(json)
} else {
showError()
}
} else {
showError()
}
} else {
showError()
}
}
func showError() {
let ac = UIAlertController(title: "Loading error", message: "There was a problem loading the feed; please check your connection and try again.", preferredStyle: .Alert)
ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(ac, animated: true, completion: nil)
}
func parseJSON(json: JSON) {
for result in json["playlists"].arrayValue {
let title = result["title"].stringValue
let id = result["id"].stringValue
let cover_url = result["cover_url"].stringValue
let obj = ["title": title, "id": id, "cover_url" : cover_url]
playlists.append(obj)
}
tableView.reloadData()
}
Use NSURLSession dataTaskWithURL for asynchronously task:
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "http://xxxxxxx.xxx/api/v1/players/1/playlists?api_key=xxxxxxxxxxxx"
if let url = NSURL(string: urlString) {
let session = NSURLSession.sharedSession()
var task = session.dataTaskWithURL(url) { (data, response, error) -> Void in
if let err = error {
showError(err)
} else {
let json = NSString(data: data, encoding: NSUTF8StringEncoding)
// json is a String, you should handle this String as JSON
parseJSON(json)
}
}
}
Your tableView.reloadData() should be executed in main thread (because the NSURLSession dataTaskWithUrl result is in background thread)
dispatch_async(dispatch_get_main_queue(), {
tableView.reloadData()
})

How to stop MBProgressHUD and add a subview when the server is not returning data

In my app I have this class to get data from my server:
class Api{
func loadOffers(completion:(([Offers])-> Void), offer_id: String, offerStatus:String){
let myUrl = NSURL(string: "http://www.myServer.php/api/v1.0/offers.php")
let request = NSMutableURLRequest(URL: myUrl!)
request.HTTPMethod = "POST"
let postString = "offer_id=\(offer_id)&offerStatus=\(dealStatus)&action=show"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
{ data, response, error in
if error != nil {
println("error\(error)")
}else{
var err:NSError?
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
if let dict = jsonObject as? [String: AnyObject] {
if let myOffers = dict["offers"] as? [AnyObject] {
var offers = [Offers]()
for offer in myOffers{
let offer = Offers(dictionary: offer as! NSDictionary)
offers.append(offer)
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0 )){
dispatch_async(dispatch_get_main_queue()){
completion(offers)
}
}
}
}
}
}
}
task.resume()
}
}
then in my View Controller I load the model:
class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var offers: [Offers]!
func loadModel() {
let loadingNotification = MBProgressHUD.showHUDAddedTo(self.view, animated: true)
loadingNotification.mode = MBProgressHUDMode.Indeterminate
loadingNotification.labelText = "updating your offers..."
offers = [Offers]()
let api = Api()
api.loadOffers(didLoadOffers , offer_id: dealIdent!, offerStatus: "open")
}
func didLoadOffers(offers:[Offers]){
self.offers = offers
self.tableView.reloadData()
MBProgressHUD.hideAllHUDsForView(self.view, animated: true)
self.refreshControl.endRefreshing()
}
override func viewWillAppear(animated: Bool) {
loadModel()
}
}
Everything works except that when the JSON dictionary is empty, meaning that there no offers the MBProgressHUD keep spinning.
I would like stop the activity indicator adding a subview instead which says that there are no offers. Any Suggestion would be greatly appreciated.
I tried:
if offers.isEmpty{
MBProgressHUD.hideAllHUDsForView(self.view, animated: true)
}
and also
if offers == 0 {
MBProgressHUD.hideAllHUDsForView(self.view, animated: true)
}
but it's not working
Thanks
This is happening because you set the HUD in main queue but are trying to remove from another one. All UI related changes should be done in main_queue()
Try using this code
dispatch_async(dispatch_get_main_queue(), {
// your code to modify HUD here
});
I recommend small redesign of code. I modified your original code a little. Here is API class:
class Api {
func loadOffers(offerID : String, offerStatus : String, completionHandler : ([Offer]?, NSError?) -> Void) {
let myURL = NSURL(string: "http://www.myServer.php/api/v1.0/offers.php")!
var request = NSMutableURLRequest(URL: myURL)
request.HTTPMethod = "POST"
let postString = "offer_id=\(offerID)&offerStatus=\(offerStatus)&action=show"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (result : NSData!, response : NSURLResponse!, error : NSError!) -> Void in
if let existingError = error {
NSLog("error \(existingError.code) - \(existingError.localizedDescription)")
completionHandler(nil, existingError)
} else {
var parseError : NSError?
if let dictionary = NSJSONSerialization.JSONObjectWithData(result, options: .allZeros, error: nil) as? [String : AnyObject] {
if let myOffers = dictionary["offers"] as? [NSDictionary] {
var parsedOffers = [] as [Offer]
for jsonOffer in myOffers {
parsedOffers.append(Offer(dictionary: jsonOffer))
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
completionHandler(parsedOffers, nil)
}
}
} else {
NSLog("JSON parsing failed")
parseError = NSError(domain: "MyApp", code: 1, userInfo : nil)
completionHandler(nil, parseError)
}
}
}).resume()
}
}
Change is added calling of completion handler even in case error with network communication and in case of failure of JSON parsing.
Corresponding implementation of VC follows:
class ViewController: UITableViewController {
private var _offers : [Offer]?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
loadModel()
}
private func loadModel() {
var hud = MBProgressHUD.showHUDAddedTo(self.view, animated: true)
hud.mode = MBProgressHUDMode.Indeterminate
hud.labelText = "updating your offers"
Api().loadOffers("1", offerStatus: "1") { (offers : [Offer]?, error : NSError?) -> Void in
self._offers = offers
// TODO: Correct handling of error state ;)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
hud.hide(true)
}
}
}
}
From my point of view is using closure better, but depends on your implementation. You can use your implementation with method instead of closure and just define hud variable on instance level.
I hope it helps you to solve your problem (I've tested on simulator and iPhone and works well with some testing stub framework).
if myOffers = nil cannot do complete(offers). so HUD could not stop. You can try this:
if let myOffers = dict["offers"] as? [AnyObject] {
var offers = [Offers]()
for offer in myOffers{
let offer = Offers(dictionary: offer as! NSDictionary)
offers.append(offer)
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0 )){
dispatch_async(dispatch_get_main_queue()){
completion(offers)
}
}
}
}
} else {
dispatch_async(dispatch_get_global_queue(priority, 0 )){
dispatch_async(dispatch_get_main_queue()){
completion([Offers]())
}
}
Every path in loadOffers needs to call the completion closure. If you have an if let statement, you need to consider the else case. If the optional is nil, you still need to call completion. You can pass an empty array in that case, or you can add an error parameter to your completion block so the view controller will know more information about why it's not getting anything back.

Best practice to hide the HUD added to view

Suppose that I have the following code:
#IBAction func signInButtonPressed(sender: AnyObject) {
MBProgressHUD.showHUDAddedTo(self.view, animated: true)
if let url = NSURL(string: someURL) {
// ...
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, error) in
if let httpError = error {
dispatch_async(dispatch_get_main_queue()) {
self.alert("Error", message: "Unable to sign in: \(httpError.localizedDescription)")
}
return
}
var deserializationError: NSError?
if let jsonData = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &deserializationError) as? [String: AnyObject] {
// ...
if let error = customer.error {
dispatch_async(dispatch_get_main_queue()) {
self.alert("Error", message: error)
}
} else {
// Show other view controller
}
} else {
if let unwrappedError = deserializationError {
dispatch_async(dispatch_get_main_queue()) {
self.alert("Error", message: "Unable to sign in: \(deserializationError)")
}
}
}
}
task.resume()
} else {
if let unwrappedError = serializationError {
self.alert("Error", message: "Unable to sign in: \(serializationError)")
}
}
}
}
What is the proper way to hide the HUD added to self.view? Is there any more elegant way to do this than adding the
dispatch_async(dispatch_get_main_queue()) {
MBProgressHUD.hideHUDForView(self.view, animated: true)
return
}
code to every if and else branches?
Thanks in advance.
first show your hud after your url initialized and right before your task get started
if let url = NSURL(string: someURL) {
MBProgressHUD.showHUDAddedTo(self.view, animated: true)
// start the request here
then hide it right after callback block started
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, error) in
dispatch_async(dispatch_get_main_queue()) {
MBProgressHUD.hideHUDForView(self.view, animated: true)
}
// here goes other logic
you don't have to call return after hud gets hidden

Resources