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
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
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.
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.
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)")
}
}
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)
}
}