Swift mutating function struct pass struct’s variable name in function?

82 views Asked by At

I am working in Swift trying to update an organization struct that will need to hold a latitude and longitude. I created a mutating function in the struct that will update the latitude and longitude based on the organization organization struct’s address. I got it to work, but the issue is that when I call the mutating function, I need to manually enter the variable name with the .latitude and .longitude. Is there a way that I can pass the variable struct’s name automatically and reference the .latitude and .longitude without calling the specific variable name with it so I can make it more usable? I included an example below with my code. Thanks for your help!

import UIKit
import PlaygroundSupport
import CoreLocation

PlaygroundPage.current.needsIndefiniteExecution = true

struct organization {
    var name: String
    var address: String
    var latitude: CLLocationDegrees = 0 //default setting for latitude
    var longitude: CLLocationDegrees = 0 //default setting for longitude
    
    mutating func getCoordinateFrom(completion: @escaping(_ coordinate: CLLocationCoordinate2D?, _ error: Error?) -> () ) {
        CLGeocoder().geocodeAddressString(address) { placemarks, error in
            completion(placemarks?.first?.location?.coordinate, error)
        }
    }
}

struct Coordinates {
    var latitude: Double
    var longitude: Double
}

//create an wildernessLodge variable of type organization
var wildernessLodge = organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830")

wildernessLodge.getCoordinateFrom { coordinate, error in
    guard let coordinate = coordinate, error == nil else { return }
    wildernessLodge.latitude = coordinate.latitude
    wildernessLodge.longitude = coordinate.longitude
    print("update 1 \(wildernessLodge)")
    }

1

There are 1 answers

3
deaton.dg On

I'm a bit confused by your code. Why is getCoordinateFrom marked as mutating? Perhaps you meant to write something like this.

mutating func getCoordinatesFromAddress() {
    CLGeocoder().geocodeAddressString(address) { placemarks, error in
        guard let coordinate = placemarks?.first?.location?.coordinate, error == nil else { return }
        self.latitude = coordinate.latitude
        self.longitude = coordinate.longitude
    }
}

Now this function is mutating (it modifies self), and

wildernessLodge.getCoordinateFrom { coordinate, error in ... }

can be replaced with

wildernessLodge.getCoordinatesFromAddress()

The only reason to leave the getCoordinateFrom method is if, somewhere in your code, you intend to get coordinates from an address but not update the coordinates in the struct. I can't imagine a good reason to do that, so I would recommend replacing the getCoordinateFrom method with something else.

Alternatively, if you generally intend to set the coordinates right after creating a value of this type, you might want to consider something like this.

init(name: String, address: String) {
    self.name = name
    self.address = address
    CLGeocoder().geocodeAddressString(address) { placemarks, error in
        guard let coordinate = placemarks?.first?.location?.coordinate, error == nil else { return }
        self.latitude = coordinate.latitude
        self.longitude = coordinate.longitude
    }
}

or

init(name: String, address: String) {
    self.name = name
    self.address = address
    self.getCoordinatesFromAddress()
}

Then, you could create an organization using organization(name: name, address: address) and the coordinates would automatically be set correctly.

If neither of these are satisfactory, maybe you should create two different structs to capture the behavior you want.

struct Organization {
    var name: String
    var address: String
    func withCoordinates(completion: @escaping(_ coordinate: CLLocationCoordinate2D?, _ error: Error?) -> () ) {
        CLGeocoder().geocodeAddressString(address) { placemarks, error in
            completion(placemarks?.first?.location?.coordinate, error)
        }
    }
}
struct OrganizationWithCoordinates {
    var name: String
    var address: String
    var latitude: CLLocationDegrees
    var longitude: CLLocationDegrees
    init(from organization: Organization) {
        self.name = organization.name
        self.address = organization.address
        organization.withCoordinates { coordinate, error in
            guard let coordinate = coordinate, error == nil else { return }
            self.latitude = coordinate.latitude
            self.longitude = coordinate.longitude
        }
    }
} 

I would prefer an approach like this, but I like having lots of types.

Finally, as noted in the comments, if you are really just concerned with brevity, you can replace

var latitude: CLLocationDegrees
var longitude: CLLocationDegrees

with

var coordinates: CLLocationCoordinate2D
var latitude: CLLocationDegrees { coordinates.latitude }
var longitude: CLLocationDegrees { coordinates.longitude }

and then replace

wildernessLodge.latitude = coordinate.latitude
wildernessLodge.longitude = coordinate.longitude

with

wildernessLodge.coordinates = coordinate

In fact, you should feel free to combine any of these approaches.

Edit: As pointed out, these solutions do not work as-is. The fundamental tension is trying to work with CLGeocoder's async method synchronously. One solution is to use a class instead of a struct. The other approach is to use a modification of the withCoordinate method above:

struct Organization {
    var name: String
    var address: String
    func withCoordinate(callback: @escaping (OrganizationWithCoordinate?, Error?) -> Void) {
        CLGeocoder().geocodeAddressString(self.address) { placemarks, error in
            if let coordinate = placemarks?.first?.location?.coordinate, error == nil {
                let orgWithCoord = OrganizationWithCoordinate(name: self.name, address: self.address, latitude: coordinate.latitude, longitude: coordinate.latitude)
                callback(orgWithCoord, nil)
            } else {
                callback(nil, error)
            }
            
        }
    }
}
struct OrganizationWithCoordinate {
    var name: String
    var address: String
    var latitude: CLLocationDegrees
    var longitude: CLLocationDegrees
}

Organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830").withCoordinate { orgWithCoord, error in
    guard let orgWithCoord = orgWithCoord, error == nil else {
        print("Error")
        return
    }
    print(orgWithCoord)
}

This embraces the async nature of CLGeocoder.

Another solution could be to force CLGeocoder to be synchronous using DispatchSemaphore as follows. I don't think these work correctly in Playgrounds, but this should work in an actual app.

struct Organization {
    var name: String
    var address: String
    var latitude: CLLocationDegrees
    var longitude: CLLocationDegrees
    
    init(name: String, address: String) throws {
        self.name = name
        self.address = address
        
        var tempCoordinate: CLLocationCoordinate2D?
        var tempError: Error?
        
        let sema = DispatchSemaphore(value: 0)
        CLGeocoder().geocodeAddressString(address) { placemarks, error in
            tempCoordinate = placemarks?.first?.location?.coordinate
            tempError = error
            
            sema.signal()
        }
        // Warning: Will lock if called on DispatchQueue.main
        sema.wait()
        
        if let error = tempError {
            throw error
        }
        guard let coordinate = tempCoordinate else {
            throw NSError(domain: "Replace me", code: -1, userInfo: nil)
        }
        
        self.longitude = coordinate.longitude
        self.latitude = coordinate.latitude
    }
}

// Somewhere in your app
let queue = DispatchQueue(label: "Some queue")
queue.async {
    let wildernessLodge = try! Organization(name: "Disney's Wilderness Lodge", address: "901 Timberline Dr, Orlando, FL 32830")

    DispatchQueue.main.async {
        print(wildernessLodge)
    }
}

Here, a new queue to do Organization related work is created to avoid locking up the main queue. This method creates the least clunky-looking code in my opinion, but probably is not the most performant option. The location APIs are async for a reason.