To me, thinking of a solution that successfully decouples code is one of the most satisfying aspects of software development. I had that satisfaction recently as I am building a new iOS app that uses CoreLocation. In this post, I will talk about how to decouple iOS code, how to access location without CoreLocation such that you can keep the UIViewController free off a CoreLocation import via delegates. In addition to that, I have also created a little Github repo to showcase sample code for the solution, read on to get the link to it.
Background
Ever since I have started working with iOS apps, my goal has been to keep the UIViewController code free of any imports other than UIKit and Foundation. I could successfully accomplish that for the most part, except when it came to working with CoreLocation. For some reason, every solution I came up with, I had to include an “import CoreLocation” in the UIViewController which was against what I wanted.
Motivation
First the personal reason,
In case anyone reading this is thinking, why do I want to achieve this (i.e. decouple code)? The main reason, well I am just lazy, writing the same code again and again is a little boring. Also being the only programmer in my startup means, I have to do all the coding so I just want to avoid re-writing the same code where possible.
Now for an emotionless, more technically sound explanation with examples
Say, if we have a code base (or app) where we can get the user’s location, physical address etc with a simple function call to a LocationHelper class. Then we can do that in in any ViewController or any other class, we don’t care how the location class gets the location as long as we know we will get the location. This means that we can use Apple, Google, Bing Maps or change our method of acquiring the solution without affecting our ViewController code. Lastly, this also means that we can use our LocationHelper class in any app that needs that functionality by importing the LocationHelper code.
Decouple iOS code
To get the location in iOS, you need to start updating location from a CLLocationManager class after which you get the location coordinates in CLLocationManagerDelegate.didUpdateLocations method. My problem was, even if I wrote a LocationHelper class with a method that calls CLLocationManager.startUpdatingLocation() how do I get the location information from the CLLocationManagerDelegate without importing a CoreLocation class.
All this time, there was an obvious solution that I simply didn’t see i.e. Delegates.
Delegate pattern
One way of thinking about Delegate pattern is to think of it as an alternative to inheritance. It’s when you delegate the responsibility of achieving something to someone else i.e. for an object to communicate back to it’s parent object. hmm maybe I am not clear? I know what this is, but I can’t think of a “layman’s terms” English explanation for it right now.
The delegate pattern is heavily used in iOS and knowingly or unknowingly every iOS developer has to have used it. For example, UITableViewDelgate, UITextViewDelegate, CLLocationManagerDelegate etc etc. This is why I cannot believe I did not think of this solution sooner! Actually, I can believe that, I had been so occupied with all things Product Management at my startup, that I was unable to just sit-down and give this careful thought.
Delegates with Protocol
A Protocol in Swift is what an interface is in Java i.e. a blue print for methods and properties of a class. In Swift we can also use them to implement Delegate pattern e.g. let’s have a look at the protocol for our solution
protocol LocationUpdatesDelegate {
func locationUpdated(lat: Double, lon: Double)
}
So when the location is updated, it notifies all the classes that adopt that protocol with the latest location. Let’s see how we use the Protocol, first in the LocationHelper class
var locationManager: CLLocationManager? = nil var locationUpdatesDelegate: LocationUpdatesDelegate? override init() { super.init() locationManager = CLLocationManager() locationManager!.requestWhenInUseAuthorization() locationManager!.delegate = self locationManager!.startUpdatingLocation() } //MARK: Location manager delegate methods func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { for location in locations { let lat = location.coordinate.latitude let lon = location.coordinate.longitude locationUpdatesDelegate?.locationUpdated(lat: lat, lon: lon) } }
In the CLLocationManagerDelgate method as soon as we get the location, we call the locationUpdated method of our delegate by passing in the latest coordinates. Next, let’s look at the UIViewController that adopts that Protocol
import UIKit
class ViewController: UIViewController, LocationUpdatesDelegate {
var locationHelper: LocationHelper? = nil
override func viewDidLoad() {
super.viewDidLoad()
locationHelper = LocationHelper()
locationHelper?.locationUpdatesDelegate = self
// Do any additional setup after loading the view.
}
func locationUpdated(lat: Double, lon: Double) {}
}
As you can see our ViewController conforms to our LocationUpdatesDelegate and implements it’s locationUpdated method. Notice, how the lat and lon parameters are of type Double, and not CLLocationCoordinate2D? This is just to keep our UIViewController free of any CoreLocation import. See, UIKit is the only import in our UIViewController class. What happens in the locationUpdated method is explained below.
The physical address
Okay, one last thing, there’s some simple code in that Github repo, that I should explain. Once we get the latitude (lat) and longitude (lon) of the user location, how do we get the physical address of the user? I have defined a struct just for convenience in the LocationHelper called Address
struct Address {
//setting them as optional because
//sometimes the GeoCoder cannot find
//these from a placemark
var name: String? = nil
var postCode: String? = nil
var locality: String? = nil
var city: String? = nil
var country: String? = nil
var state: String? = nil //could be state or province
func toString() -> String {
if let n = name,
let cty = city,
let ctry = country {
return "\(n), \(cty) (\(ctry))"
}
return "\(name ?? ""), \(locality ?? ""), \(city ?? ""), \(state ?? ""), \(postCode ?? "") \(country ?? "")"
}
}
We use Address , as a return type in the getCoordinateAddress completion handler.
func getCoordinateAddress(lat: Double, lon: Double, completion: @escaping (_ address: Address?) -> ()) {
let location = CLLocation(latitude: lat, longitude: lon)
CLGeocoder().reverseGeocodeLocation(location) { (placemarks, error) in
if error != nil {
return
}
if let placesFound = placemarks {
for place in placesFound {
var address = Address()
address.city = place.locality
address.country = place.country
address.postCode = place.postalCode
address.name = place.name
address.state = place.administrativeArea
completion(address)
}
}
}
}
A delegate could potentially be used for this too but for this one, we don’t need to. I mean the completion handler works just fine. Lastly, here’s how we use that in our UIViewController
class ViewController: UIViewController, LocationUpdatesDelegate {
@IBOutlet var locationLbl: UILabel!
var address: Address? = nil {
willSet {
locationLbl.text = newValue?.toString()
}
}
func locationUpdated(lat: Double, lon: Double) {
locationHelper?.getCoordinateAddress(lat: lat, lon: lon, completion: { (reverseGeocodedAddress) in
if reverseGeocodedAddress != nil {
self.address = reverseGeocodedAddress
}
})
}
}
The above code is self explanatory, as we just use one of Swift’s property observer to se the text for a UILabel.
Conclusion
That’s a wrap! You can find all the code for this post on this Github repository here. Have a look at the LocationHelper class, you could jus re-use it or use as a starting point in your iOS app. Give that Github repo a star if you find it useful.
As usual, if you find any of my posts useful support me by buying or even trying one of my apps on the App Store.
Also, if you can leave a review on the App Store or Google Play Store, that would help too.
0 Comments