DispatchGroup with Asynchronous function - ios

I use Alamofire for get a request. I have two UIViewControllers and I use prepare (segue) function for send the data between the both.
On my first view controller, I use Alamofire but when I use prepare (segue), all my informations are empty.
#IBAction func loginPage(_ sender: UIButton) {
let group = DispatchGroup()
Helper().alomofirePost(URL: "http://192.168.1.7/app_dev.php/login_check", Paramaters: paramaters) { contenuJSON in
if (contenuJSON["connected"].stringValue == "true") {
group.enter()
self.dashboad()
group.leave()
group.notify(queue: DispatchQueue.main) {
//print(self.image) // EMPTY
print(self.info[0]) // EMPTY FATAL ERROR INDEXT OUT OF RANGE
self.performSegue(withIdentifier: "Dashboard", sender: self)
}
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "Dashboard" {
let success = segue.destination as! DashboardViewController
success.profil = self.image
}
}
func dashboad() {
// Other Function
//self.image = addPicProfil()
self.info = add_info(url: "http://192.168.1.7/app_dev.php/dashboard/info")
}
func add_info(url: String) -> [String] {
var info = [String]()
Helper().alomofireGet(URL: url) { contentJSON in
var content = contentJSON
print(content)
info.append(contentJSON["userFirstName"].stringValue)
info.append(contentJSON["countDevices"].stringValue)
info.append(contentJSON["earnedThisYearsEUR"].stringValue)
info.append(contentJSON["countCampaigns"].stringValue)
}
return (info)
}
In my Helper File I have :
func alomofireGet(URL: String, onCompletion:#escaping ((JSON) -> Void)) {
var contentJSON = JSON()
Alamofire.request(URL, method: .get).responseJSON() { (reponse) in
if reponse.result.isSuccess {
contentJSON = JSON(reponse.result.value!)
} else {
contentJSON = JSON(reponse.result.error!)
}
onCompletion(contentJSON)
}
}
func alomofirePost(URL: String, Paramaters: Dictionary<String, Any>, onCompletion: #escaping ((_ response: JSON) -> Void)) {
Alamofire.request(URL, method: .post, parameters: Paramaters).validate().responseJSON { (reponse) in
var contenuJSON = JSON()
if reponse.result.isSuccess {
contenuJSON = JSON(reponse.result.value!)
} else {
contenuJSON = JSON(reponse.result.error!)
}
onCompletion(contenuJSON)
}
}

You mess use DispatchQueue info is nil as you think that this
self.info = add_info(url: "http://192.168.1.7/app_dev.php/dashboard/info")
will add the asynchronous values appended but it will return an empty array , you need
func add_info(url: String,completion:#escaping(_ arr:[String]) -> ()) {
var info = [String]()
Helper().alomofireGet(URL: url) { contentJSON in
print(contentJSON)
info.append(contentJSON["userFirstName"].stringValue)
info.append(contentJSON["countDevices"].stringValue)
info.append(contentJSON["earnedThisYearsEUR"].stringValue)
info.append(contentJSON["countCampaigns"].stringValue)
completion(info)
}
}

Related

WKWebView delegate not responding to delegate method call

I have a WKWebView that is used to present the login screen of my OAuth Identity provider.
import UIKit
import WebKit
protocol OAuth2WKWebViewDelegate: class {
func didReceiveAuthorizationCode(_ code: String) -> Void
func didRevokeSession() -> Void
}
class OAuth2WKWebViewController: UIViewController {
let targetUrl: URLComponents
let webView = WKWebView()
weak var delegate: OAuth2WKWebViewDelegate?
init(targetUrl: URLComponents) {
self.targetUrl = targetUrl
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
loadUrl()
}
}
extension OAuth2WKWebViewController: WKNavigationDelegate {
func loadUrl() {
guard let url = targetUrl.url else { return }
view = webView
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
webView.navigationDelegate = self
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
if url.scheme == "appdev", url.absoluteString.range(of: "code") != nil {
let urlParts = url.absoluteString.components(separatedBy: "?")
let code = urlParts[1].components(separatedBy: "code=")[1]
delegate?.didReceiveAuthorizationCode(code)
}
if url.absoluteString == "appdev://oauth-callback-after-sign-out" {
delegate?.didRevokeSession()
}
}
decisionHandler(.allow)
}
}
I also have an IdentityService I use to present this view and respond to it's success / error.
protocol IdentityServiceProtocol {
var hasValidToken: Bool { get }
func initAuthCodeFlow() -> Void
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void
func storeOAuthTokens(accessToken: String, refreshToken: String, completion: #escaping () -> Void) -> Void
func renderAuthView() -> Void
}
class IdentityService: IdentityServiceProtocol {
fileprivate var apiClient: APIClient
fileprivate var keyChainService: KeyChainService
init(apiClient: APIClient = APIClient(), keyChainService: KeyChainService = KeyChainService()) {
self.apiClient = apiClient
self.keyChainService = keyChainService
}
var hasValidToken: Bool {
return keyChainService.fetchSingleObject(withKey: "AccessToken") != nil
}
func initAuthCodeFlow() -> Void {
let queryItems = ["response_type": "code", "client_id": clientId, "redirect_uri": redirectUri, "state": state, "scope": scope]
renderOAuthWebView(forService: .auth(company: "benefex"), queryitems: queryItems)
}
func initRevokeSession() -> Void {
guard let refreshToken = keyChainService.fetchSingleObject(withKey: "RefreshToken") else { return }
let queryItems = ["refresh_token": refreshToken]
renderOAuthWebView(forService: .revokeSession(company: "benefex"), queryitems: queryItems)
}
func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void {
guard let targetUrl = constructURLComponents(endPoint: service, queryItems: queryitems) else { return }
let webView = OAuth2WKWebViewController(targetUrl: targetUrl)
webView.delegate = self
UIApplication.shared.windows.first?.rootViewController = webView
}
func storeOAuthTokens(accessToken: String, refreshToken: String, completion: #escaping ()-> Void) -> Void {
let success = keyChainService.storeManyObjects(["AccessToken": accessToken, "RefreshToken": refreshToken])
guard success == true else { return }
completion()
}
func renderAuthView() -> Void {
UIApplication.shared.windows.first?.rootViewController = UINavigationController.init(rootViewController: AuthenticatedViewController())
}
}
extension IdentityService: OAuth2WKWebViewDelegate {
func didReceiveAuthorizationCode(_ code: String) {
apiClient.call(endpoint: IdentityEndpoint.accessToken(company: "benefex", code: code)) { [weak self] (response: OAuthTokenResponse) in
switch response {
case .success(let payload):
guard let accessToken = payload.accessToken, let refreshToken = payload.refreshToken else { return }
self?.storeOAuthTokens(accessToken: accessToken, refreshToken: refreshToken) { self?.renderAuthView() }
case .error:
// login failed for some reason
print("could not complete request for access token")
}
}
}
func didRevokeSession() {
print("This was called")
}
}
extension IdentityService {
fileprivate var state: String {
return generateState(withLength: 20)
}
fileprivate func constructURLComponents(endPoint: IdentityEndpoint, queryItems: [String: String]) -> URLComponents? {
var url = URLComponents(url: endPoint.baseUrl, resolvingAgainstBaseURL: false)
url?.path = endPoint.path
url?.queryItems = queryItems.map { URLQueryItem(name: $0.key, value: $0.value) }
return url
}
fileprivate func generateState(withLength len: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let length = UInt32(letters.count)
var randomString = ""
for _ in 0..<len {
let rand = arc4random_uniform(length)
let idx = letters.index(letters.startIndex, offsetBy: Int(rand))
let letter = letters[idx]
randomString += String(letter)
}
return randomString
}
}
extension IdentityService {
var clientId: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_CLIENT_ID"] else { fatalError("Missing APP_CLIENT_ID enviroment variable") }
return value
}
var redirectUri: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_REDIRECT_URI"] else { fatalError("Missing APP_REDIRECT_URI enviroment variable") }
return value
}
var scope: String {
let envVar = ProcessInfo.processInfo.environment
guard let value = envVar["APP_SCOPES"] else { fatalError("Missing APP_SCOPES enviroment variable") }
return value
}
}
The delegate?.didReceiveAuthorizationCode(code) is working.
However when delegate?.didRevokeSession() is called from the WebView, the identity service does not respond.
I added some console logs and can see my IdentityService is being de - init when I invoke the logout method.
I believe this is causing it to do nothing when the delegate method fires.
How can I ensure the delegate method is called still?
This came up when I was searching for answers to my issue - if you are using the 10.2 or 10.2.1 compiler - an issue was occurring for us when compiling in Release (instead of Debug) - where the delegate functions are not being called
The fix for us was to include #objc before all delegate function calls, IE
#objc func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
If you are in the same scenario - this should help

How can i check a server response value before performing the segue

Here is my code in UploadModel.swift
func uploadSpecialistWithId(login: String, password: String) {
let requestUrl = URL(string: "http://localhost:3000/api/register/specialist")!
var request = URLRequest(url: requestUrl)
request.httpMethod = "POST"
let postParams = "login=\(login)&password=\(password)"
print(postParams)
request.httpBody = postParams.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) {(data, response, error)
in
if(error != nil) {
print("error -> \(error!.localizedDescription)")
} else {
print("data uploaded")
self.parseJSONSpecialistResponse(data: data!)
}
}
task.resume()
}
func parseJSONSpecialistResponse(data: Data) {
var jsonResult = NSDictionary()
let specialist = Specialist()
do {
jsonResult = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! NSDictionary
} catch let error as NSError {
print("error -> \(error.localizedDescription)")
}
let items = NSMutableArray()
if let login = jsonResult["login"] as? String,
let specialistId = jsonResult["_id"] as? String {
specialist.login = login
specialist.specialistId = specialistId
print("Added \(specialist.itemDescribtion())")
items.add(specialist)
} else {
let errorCode = jsonResult["code"] as! Int
print("Erorr with code -> \(errorCode)")
items.add(errorCode)
}
DispatchQueue.main.async(execute: { () -> Void in
self.delegate.itemsUploaded(items: items)
})
}
But when i was trying to reg the specialist with existing login the segue had performed before i handled the server response. What should I change to solve this problem? Here is my code in ViewController.swift
func itemsUploaded(items: NSArray) {
if let errorCode = items[0] as? Int {
if errorCode == 11000 {
warningLabel.text = "This login is already used"
error = true
}
}
if let specialist = items[0] as? Specialist {
self.specialistId = specialist.specialistId!
print("Value after itemsUploaded --> \(self.specialistId)")
}
}
#IBAction func doneButtonTouched(_ sender: Any) {
uploadModel.uploadSpecialistWithId(login: loginTextField.text!, password: passwordTextField.text!)
if(error == false) {
print("Segue perform value -> \(specialistId)")
performSegue(withIdentifier: "regToRegCompleteSegue", sender: self)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destination = segue.destination as! RegistrationCompleteViewController
print("Segue prepare value -> \(specialistId)")
destination.specialistId = self.specialistId
}
I understand that i have this issue because of asynchronously of dataTask. But why does it not working when the self.delegate is already in the main queue?
EDIT: Adding a completionHandler didn't solve my problem, but Kamran's offer is working.
You should perform segue when you are done with response by moving the below lines in itemsUploaded as below,
#IBAction func doneButtonTouched(_ sender: Any) {
uploadModel.uploadSpecialistWithId(login: logiTextField.text!, password: passwordTextField.text!)
}
func itemsUploaded(items: NSArray) {
if let errorCode = items[0] as? Int {
if errorCode == 11000 {
warningLabel.text = "This login is already used"
error = true
}
}
if let specialist = items[0] as? Specialist {
self.specialistId = specialist.specialistId!
print("Value after itemsUploaded --> \(self.specialistId)")
}
if(error == false) {
print("Segue perform value -> \(specialistId)")
performSegue(withIdentifier: "regToRegCompleteSegue", sender: self)
}
}
You should call performSegue inside the itemsUploaded, once it will only be called when the data is ready.
Another way that you could do this is by using #escaping in your function parseJSONSpecialistResponse to know when it is completed. Inside the block of the completion, you can then call performSegue.

Preform segue inside closure

I am trying to display the value of btc in a separate view controller but EthViewController is not changing if I set the label equal to btc inside of a closure.
func btcValue(completion: #escaping((String) -> ())){ //Added Line
let url = URL(string: "https://api.coindesk.com/v1/bpi/currentprice.json")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print ("Error!")
} else {
if let content = data {
do {
let myJson = try JSONSerialization.jsonObject(with: content) as! [String:Any]
if let rates = myJson["bpi"] as? [String:Any] {
if let currency = rates["USD"] as? [String:Any] {
if let btc = currency["rate"] as? String {
completion(btc) //Added Line
}
}
}
}
catch{
print(error)
}
}
}
}
task.resume()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let EthViewController = segue.destination as! EthViewController
btcValue { (btc) in
print(btc)
EthViewController.ethprice_string = btc
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
performSegue(withIdentifier: "segue_eth", sender: self)
}
Is there anyway that I can preform a segue from inside the closure or return the value btc outside of the closure? Any help is greatly appreciated.

Turn a String value to a URL value

im redoing my ode after getting mixed up with some concepts of swift. Have in mind im new to swift.
In my project there are currently 2 ViewControllers, in the first one there is a UITextField and a UIButton. in the second one there is a UIWebView.
I know that the UIWebView only allows a URL type address, so i want the text introduced in the UITextField to be a URL, So how do i change the string introduced into a URL and display that value (URL introduced) in the UIWebView? Should i store that value in a global variable? I really tried all...
You can send it to the nextVC via Segue , or store it in defaults and read it there
let myUrl = URL(string:textfield.text)
self.performSegue(withIdentifier: "goToNext", sender:myUrl)
//
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let let next = segue.destination as? nextVC {
next.currentUrl = sender as! URL
}
}
//
class nextVC : UIViewController
{
var currentUrl:URL?
}
It is very important to check if the user has entered a valid url. you can check that in Second viewController while loading the request from url.
Here is the overall code :
First viewController :
var url: String!
url = URL(string:textfield.text) // Convert text to url
self.performSegue(withIdentifier: "yourIdentifier", sender:url) // Go from one VC to other.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let let vc = segue.destination as? nextVC {
vc.url = self.url
}
}
Second viewController :
var url: String! // Global var
if url.isUrl
{
webView.loadRequest(URLRequest(url: url!))
}
else
{
print("invalid url")
}
Validate URL string extension :
extension String {
var isUrl: Bool {
// for http://regexr.com checking
// (?:(?:https?|ftp):\/\/)(?:xn--)?(?:\S+(?::\S*)?#)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[#-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?
let schemes = URLSchemes.getAllSchemes(separetedBy: "|").replacingOccurrences(of: "://", with: "")
let regex = "(?:(?:\(schemes)):\\/\\/)(?:xn--)?(?:\\S+(?::\\S*)?#)?(?:(?!10(?:\\.\\d{1,3}){3})(?!127(?:\\.\\d{1,3}){3})(?!169\\.254(?:\\.\\d{1,3}){2})(?!192\\.168(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[#-z\\u00a1-\\uffff]{2,})))(?::\\d{2,5})?(?:\\/[^\\s]*)?"
let regularExpression = try! NSRegularExpression(pattern: regex, options: [])
let range = NSRange(location: 0, length: self.characters.count)
let matches = regularExpression.matches(in: self, options: [], range: range)
for match in matches {
if range.location == match.range.location && range.length == match.range.length {
return true
}
}
return false
}
var toURL: URL? {
let urlChecker: (String)->(URL?) = { url_string in
if url_string.isUrl, let url = URL(string: url_string) {
return url
}
return nil
}
if !contains(".") {
return nil
}
if let url = urlChecker(self) {
return url
}
let scheme = URLSchemes.detectScheme(urlString: self)
if scheme == .unknown {
let newEncodedString = URLSchemes.http.rawValue + self
if let url = urlChecker(newEncodedString) {
return url
}
}
return nil
}
}
Here is an example of how to do this.
Here is the gist:
ViewController:
class ViewController: UIViewController, UIWebViewDelegate {
#IBOutlet weak var urlTextView: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let webViewController = segue.destination as? WebViewController {
if let url = sender as? URL {
webViewController.urlToLoad = url
}
}
}
#IBAction func gotoURLButtonAction(_ sender: Any) {
let url = self.urlTextView.text ?? ""
if url.count > 0 {
if isProperHTTPUrl(str: url) {
self.performSegue(withIdentifier: "ViewControllerToWebViewController", sender: URL(string: url))
} else {
showError("not proper url format")
}
} else {
showError("must set url")
}
}
func isProperHTTPUrl(str:String) -> Bool {
let re = try! NSRegularExpression(pattern: "(?i)https?:\\/.*", options: [])
return re.numberOfMatches(in: str, options: .anchored, range: NSRange(location: 0, length: str.count)) > 0
}
}
WebViewController:
class WebViewController: UIViewController {
var urlToLoad:URL?
#IBOutlet weak var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let url = self.urlToLoad {
self.webView.loadRequest(URLRequest(url: url))
}
}
public func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {
showError("Unable to load, \(error)")
}
}

How to asynchronous call an api and update UI on success?

i was wondering what's the right design to:
Make an API call
Create a model of the result
Load the image of the model
Wait for everything to be loaded ..and then update the ui
I used (nested) delegation and a dispatch_group. But i am sure, you might have some tips how to do this right, don't you?
Here is my working code:
View Controller
#IBAction func loadLoaction(sender: AnyObject) {
placeManager.loadRandomLoaction(lat, longitude: long)
}
func placeDidLoad(place: Place) {
nameLabel.text = place.name
imageView.image = place.image
}
Classes
import Foundation
import Alamofire
extension Alamofire.Request {
class func imageResponseSerializer() -> Serializer {
return { request, response, data in
if data == nil {
return (nil, nil)
}
let image = UIImage(data: data!, scale: UIScreen.mainScreen().scale)
return (image, nil)
}
}
func responseImage(completionHandler: (NSURLRequest, NSHTTPURLResponse?, UIImage?, NSError?) -> Void) -> Self {
return response(serializer: Request.imageResponseSerializer(), completionHandler: { (request, response, image, error) in
completionHandler(request, response, image as? UIImage, error)
})
}
}
public protocol PlaceDelegate: class {
func placeDidLoad(place: Place)
}
public class Place : NSObject {
var delegate: PlaceDelegate?
public var name: String?
public var category: String?
public var image_url: String?
public var snippet_text: String?
public var address: String?
public var distance: String?
public var url: String?
public var image: UIImage = UIImage()
var dispatch_group = dispatch_group_create()
init(fromResponseDict responseDict: Dictionary<String, String>, delegate: PlaceDelegate) {
self.delegate = delegate
// Set props
self.name = responseDict["name"]
self.category = responseDict["category"]
self.image_url = responseDict["image_url"]!
self.snippet_text = responseDict["snippet_text"]
self.url = responseDict["url"]
self.address = responseDict["address"]!
super.init()
// Load image
self.loadImage()
// Dispatch if success
dispatch_group_notify(self.dispatch_group, dispatch_get_main_queue(), {
self.delegate?.placeDidLoad(self)
})
}
// (3) Load the image
func loadImage() {
dispatch_group_enter(self.dispatch_group);
var request:Alamofire.Request = Alamofire.request(.GET, self.image_url!).responseImage() {
(request, _, image, error) in
if error == nil && image != nil {
NSLog("imageRequestSuccess")
self.image = image!
// Dispatch if success
dispatch_group_leave(self.dispatch_group)
} else {
NSLog("imageRequestFailure")
// Dispatch also to handle failure
dispatch_group_leave(self.dispatch_group)
}
}
}
}
public protocol PlaceManagerDelegate: class {
func placeDidLoad(place: Place)
}
public class PlaceManager : NSObject, PlaceDelegate {
public weak var delegate: PlaceManagerDelegate?
var isLoading = false
enum Router: URLRequestConvertible {
static let baseURLString = "http://api.domain.com"
// Endpoints
case RandomPlace(lat:Double, long:Double)
var URLRequest: NSURLRequest {
let (path: String, parameters: [String: AnyObject]) = {
switch self {
case .RandomPlace (let lat, let long):
let params : [ String : AnyObject] = ["ll": "\(lat),\(long)", "", "debug":"true"]
return ("/getRandomLocation", params)
}
}()
let URL = NSURL(string: Router.baseURLString)
let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
public func loadRandomLoaction(latitude:Double, longitude:Double) {
if isLoading {
return
}
isLoading = true
// (1) Make the API Call
Alamofire.request(PlaceManager.Router.RandomPlace(lat: latitude, long: longitude)).responseJSON() {
(_, _, JSON, error) in
if error == nil {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
if let responseDict = JSON as? Dictionary<String, String> {
// (2) Create the model
let place = Place(fromResponseDict: responseDict, delegate:self)
}
}
} else {
NSLog(error!.localizedDescription)
}
self.isLoading = false
}
}
// (4) Update the UI
public func placeDidLoad(place: Place) {
self.delegate?.placeDidLoad(place)
}
}

Resources