Saturday, May 13, 2017

Recreating Apple's current location dot with Google Maps


A few months ago I was tasked with recreating the stock current location dot from Apple Maps with the Google Maps iOS SDK. At the time, the SDK didn't support animated views on the map. This meant I had to do some hackry where I was placing UIViews on top of the map view, and then synchronising their positions to match the map coordinates of the map view. It was ugly. The main problem with this approach is that there was a delay between the map moving, and the relevant callback to update the dot's position.

Since then Google maps for iOS has come along way. I've noticed that Uber's app doesn't have this delay (for the dot, the cars still lag), so I thought it was worthwhile having another crack at the implementation.


//
// ViewController.swift
// CurrentLocation
//
// Created by Jonathan Dalrymple on 13/05/2017.
// Copyright © 2017 float-right. All rights reserved.
//
import UIKit
import GoogleMaps
class CurrentLocationView: UIView {
private class CircularView: UIView {
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
private var shapeLayer: CAShapeLayer? {
get {
if let shape = self.layer as? CAShapeLayer {
return shape
}
return nil
}
}
override init(frame: CGRect) {
super.init(frame: frame)
super.backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(roundedRect: self.bounds,
cornerRadius: self.bounds.width * 0.5).cgPath
self.shapeLayer?.path = path
}
override var backgroundColor: UIColor? {
set (newValue){
self.shapeLayer?.fillColor = newValue?.cgColor
}
get {
if let color = self.layer.backgroundColor {
return UIColor(cgColor: color)
}
return nil
}
}
}
private let baseView = CircularView()
private let dotView = CircularView()
private let rangeView = CircularView()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.rangeView)
self.addSubview(self.baseView)
self.addSubview(self.dotView)
self.rangeView.backgroundColor = UIColor.blue.withAlphaComponent(0.25)
self.baseView.backgroundColor = .white
self.dotView.backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.rangeView.bounds = self.bounds
self.rangeView.center = self.center
self.baseView.bounds = CGRect(x: 0, y: 0, width: 32, height: 32)
self.baseView.center = self.center
self.dotView.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
self.dotView.center = self.center
}
func startAnimating() {
let dotPulse = CABasicAnimation(keyPath: "transform")
dotPulse.byValue = CATransform3DMakeScale(0.8, 0.8, 1)
dotPulse.duration = 1.25
dotPulse.repeatCount = .infinity
dotPulse.autoreverses = true
self.dotView.layer.add(dotPulse, forKey: "pulsate")
let rangePulse = CABasicAnimation(keyPath: "transform")
rangePulse.fromValue = CATransform3DMakeScale(0.1, 0.1, 1)
rangePulse.duration = 2.5
rangePulse.repeatCount = .infinity
self.rangeView.layer.add(rangePulse, forKey: "pulsate")
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.toValue = 0.0
fadeOut.duration = 2.5
fadeOut.repeatCount = .infinity
self.rangeView.layer.add(fadeOut, forKey: "fadeOut")
}
}
class ViewController: UIViewController, GMSMapViewDelegate {
lazy var mapView: GMSMapView = {
let view = GMSMapView()
view.delegate = self
return view
}()
lazy var currentLocationMarker: GMSMarker = {
let marker = GMSMarker(position: CLLocationCoordinate2D(latitude: 0, longitude: 0))
let annotationView = CurrentLocationView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
marker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
marker.iconView = annotationView
return marker
}()
override func viewDidLoad() {
super.viewDidLoad()
GMSServices.provideAPIKey("INSERT YOUR GMS API KEY")
self.view.addSubview(self.mapView)
let location = CLLocationCoordinate2D(latitude: 51, longitude: 0)
self.currentLocationMarker.position = location
self.currentLocationMarker.map = self.mapView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.mapView.frame = self.view.bounds
}
func mapViewSnapshotReady(_ mapView: GMSMapView) {
if let v = self.currentLocationMarker.iconView as? CurrentLocationView {
v.startAnimating()
}
}
}

Saturday, January 07, 2017

Travel Report: Ethiopia


Sadly not as many air miles as I would have liked

I spent about 12 days in Ethiopia over the Christmas and New year period of 2016. In a word it was fantastic. I learnt so much about a country that I knew so little about.
The total cost for my trip was £1340 (approximate $1640 USD in Jan '17). This included the direct international return flight on Ethiopian Airlines (on a brand new A350), my 4 domestic flights, all food, all accomodation, everything.

 

Addis Ababa - Metropolitan Africa

 

Merksel Square

There isn't too much to say about Ethiopia's capital. it's about 130 years old, and home to 6 million people. It's has all the amenities that one would expect of a large city, but not much in the way of history. Of course being home to that many people, there are plenty of things to do, just not so many that could be considered uniquely Ethiopian. 

 

Bahir Dar - The closest thing to a beach resort.

 

Lake Tana

Given that Ethiopia is a land locked Bahir Dar is the closest thing that they have to a domestic beach destination. The main tourist attraction is the various monasteries dotted around the lake.

 

Gonder - African Castles

 

One of the ruined castles of Gonder



The highlight of Gonder are the numerous ruined castles that form it's center. The castles are mostly from the mid 16th Century, but vary in age due to the fact that they were built during the reigns of different Emperors.

 

Lailibela - Jeruselam in Africa


The rock hewn churches of this town were the highlight of the trip. The various churches of Lailibela were originally conceived as an alternative to Jeruselam in the 12-13 century. Apparently Ethiopian christians would take a piligramage from Africa to the Holy Land. I have no idea how long that would have taken 1000 years ago, but Google clocks it as 4000km.
Bete Amanuel

Each of the churches is made from a single piece of rock. Most of the churches are surrounded by rock, beneath surface level. UNESCO built roofs over many of the structures in 2008 to protect them from natural erosion, hence the modern looking structures you see in some of the pictures.

Bete Medani Alem, with worshippers for scale
 The cost of entrance was 50USD, and it allowed you to visit all of the churches over a 5 day period. It is important to understand that they are not only huge, but also still used. Pilgrims still travel from around the country every christmas to worship here, many of them walk.



The churches are below ground level and are connected through a series of tunnels and concealed entrances. I explored the churches without a guide, and was able to find most them, without a problem. However the sheer complexity of the entire system will leave you scratching you head as to how it was built to begin with, why it isn't a better known attraction.

 

Harar - The trading hub of Eastern Ethiopia

 

"Bad eyes" gate, one of the original gates of Harar
 Harar is  home to a walled city in the East of the country, between Addis Ababa and the Somali frontier. Unlike the other cities I visited Harar is a predominately muslim city.

A Indian style merchants house
Before the Derg, and troubles of that era the town was prosperous trading outpost host to merchants and Traders from around Middle & Far East. Modern buildings are banned within the city center, and so it's easy to imagine what the city would have been like during it's heyday.

 

Food

 

I often joke that I travel to learn, sleep, and eat. Teff is the main grain of Ethiopia. Wheat flour is available, and eaten by some but for the most part it's all Teff, all the time. The Teff is usually fermented and turned into a "Pancake" called an Injera. Then the Injera is served with everything and anything.

Goran Gorad - Raw meat

I forget the name of this dish, but it's essentially Injera, soaked in butter, served with a mild pepper
This was lovely Soup in Harar, made from a mutton stock, with mutton. The tradition was to tear up the Injera and allow it soak up the soup. possibly the best thing I had in the country.

Gomen Besiga - Which is a Lamb shank, wrapped in a spinich like vetagable.
Another delious meal, I only knew how to read meat, in Amharic so I just pointed at a meat dish and hoped for the best

Conclusion 

Bole, the cosmopolitan heart of Addis Ababa
I had avoided Africa for a long time during my travels, mainly as I didn't want to go on Safari, and I didn't particularly care to see tribes people dancing around mud huts. Ethiopia provided me with the type of travel destination I love; A long and interesting past, good food, nice people, warm weather & cheap amenties. It's not quite Colombia, but it now has a prominent place in my heart.

Thursday, December 08, 2016

Operator overloading in Swift

After years of having to write [@"bar" isEqualToString:@"foo"] I’m delighted that in swift we can simplify things and just write “bar” == “foo”.

However there is an important thing to note. Swift will match the overloaded operator to function that takes the same arguments. Simple.

So if I follow the example set by the equatable protocol, and wanted to compare two NSObject subclasses, I could create a function that looks something like:

class Object: NSObject {
let name:String
init(name:String) {
self.name = name
}
}
func ==(lhs: Object, rhs:Object) -> Bool {
return lhs.name == rhs.name
}
func doSomethingWhenTheStringMatches() {
print("hello word")
}
let a = Object(name: "foo")
let b = Object(name: "foo")
if a == b {
doSomethingWhenTheStringMatches()
}
view raw exampleA.swift hosted with ❤ by GitHub
And this would work everywhere as expected and print “hello world” …

Well, no. Remember I said exactly the same arguments.

So, If I was to change the optionality of one of the variables:

let a = Object(name: "foo")
var b:Object?

b = Object(name: "foo")

Our custom equality function will no longer be called, as it only expects unwrapped values. This means that the equality would fail, as the pointers do not match, and the test would fail and doSomethingWhenTheStringChanges would never be called.

The solution is fairly simple, you need to create a version of the overridden operator that accepts optionals, but again remember that the compiler is trying to match parameters, so you also need to cover the case where you have one/two unwrapped parameters.

class Object: NSObject {
let name:String
init(name:String) {
self.name = name
}
}
func ==(lhs: Object, rhs:Object) -> Bool {
return lhs.name == rhs.name
}
func ==(lhs: Object?, rhs:Object?) -> Bool {
guard let l = lhs, let r = rhs else {
return lhs === rhs
}
return l == r
}
func doSomethingWhenTheStringMatches() {
print("Match")
}
var a:Object?
var b:Object?
if a == b {
doSomethingWhenTheStringMatches()
}
view raw exampleB.swift hosted with ❤ by GitHub
What will now happen, is that when we have unwrapped parameters it will go directly to the first overridden operator, while optionals will use the second. We use the guard to ensure that we can unwrap both, and then do a pointer equality check which will return the correct result if one, or both are nil.

What makes this behaviour particularly “special” is that it will only happen with NSObject subclasses. When using doing the same thing with a pure Swift class, if you are missing the optional variation of the overridden operator the compiler will require you to explicitly unwrap the variable.

Ultimately I think the best way to avoid this mess is to just do things the old fashioned way and override isEqual: in your subclass.

It should also be noted that Xcode correctly syntax highlights the operator depending on whether you will use NSObject’s version of isEqual: or your own at runtime, but there is no warning.

Monday, January 11, 2016

Taking back disk space from Xcode

I have 256 GB SSD in my Macbook Pro, I have no external disks, and no cloud storage other than a Dropbox's free tier. So I think it's fair to say that I'm not a data hoarder. I live my digital life, much like I live my real life, lean and light.

In the last few months I've been constantly hovering around ~8GB of available space, and more recently I've opened my Macbook to messages of "No available disk space". Considering that I dilligently manage my storage I've been a little confused as to why this is the case.

After digging around a little, I discovered that the ~/Library/Developer folder is weighing in at 35.75GB, thats 13% of my disk space!

Digging a little deeper, ~/Library/Developer/Xcode/iOS DeviceSupport/ contains the symbols for every iOS version that you've connected to your machine. If you are like me, and have been doing development for a while, you'll probably have a few, I had 12, going as far back as 7.1.2. They range in size from 600MB, all the way upto 3.36 GB. For my development needs I only need the last two versions; 9.2 & 8.4.1. Removing the unused symbols, as well some dervived data from some old projects helped me recover around 13GB.

If you've installed Xcode Betas you may have had the issue where you have duplicate simulators, sometimes even 3 copies. Each simulator is usually 1GB+ so removing excess ones can save you some additional space. On the advice of this stackoverflow post I installed the snapshot tool, which is part of the fastlane toolkit. It has a handy command called "reset_simulators" which will remove all the simulators, and recreate only the simulators for the current primary SDK you have installed.

The above tips helped me recover 22 GB just from Xcode.


Friday, November 13, 2015

My public key

I'm not quite sure why, but it's taken me a life time to setup encrypted email. Below is my Public key, email away.


-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: GPGTools - https://gpgtools.org
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
bBGMJbVcrd9l16X4b38V0pUtCmU9v5F+VQ==
=XXj2
-----END PGP PUBLIC KEY BLOCK-----
view raw PGP Key hosted with ❤ by GitHub

Friday, October 24, 2014

Making a numeric/pin pad with NSLayoutConstraints


NSLayoutConstraints are awesome. But like many cocoa technologies (strangely, all the ones that I like) the learning curve is fairly steep.

The goals was simple, I wanted to make a Numeric pin pad that would center itself in it's container view, while ensuring that all the buttons remained square, and aligned .... Ok, maybe not so simple.


Personally I hate resorting to changing the constraint priority, as I feel that most of the time it's a sign that your approaching the problem incorrectly. You can see a gist of the constraints below, ultimately I found the trick was this gem "-(>=1)-". By placing this between the outermost views and then pinning these to the superview, they provide the layout with the flexibility to meet all of it's requirements without having to adjust the superview or simply "explode".

Lastly, if your wondering why i've adopted this strange grouping mechanism inside my loop, I wanted the subview index have a 1-1 mapping with the button number, with out having to resort to setting tags. This way in the button handers I can look up the index of the sender in the subview collection, and know which button it is.

Like 99% of cocoa code, it's not concise. Disfrutarlo!
- (void)updateConstraints {
NSMutableArray *constraints = [NSMutableArray array];
NSDictionary *views;
NSDictionary *metrics = @{};
void (^squareConstraint)(UIView *) = ^(UIView *aView) {
[constraints addObject:[NSLayoutConstraint constraintWithItem:aView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:aView
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0]];
};
//Iterate in threes
for (NSUInteger idx=0; idx < self.subviews.count; idx++) {
if (idx % 3 == 0 && idx >= 1 && idx <= 9) {
views = @{
@"upperLeft": (idx > 3) ? self.subviews[idx-3] : [NSNull null], //Get the left most view on the upper row
@"left":self.subviews[idx-2],
@"center":self.subviews[idx-1],
@"right":self.subviews[idx]
};
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=1)-[left]-[center(==left)]-[right(==left)]-(>=1)-|"
options:NSLayoutFormatAlignAllCenterY|NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
metrics:metrics
views:views]];
[constraints addObject:[NSLayoutConstraint constraintWithItem:views[@"center"]
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
if (views[@"upperLeft"] == [NSNull null]) {
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[left]"
options:0
metrics:metrics
views:views]];
} else {
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[upperLeft]-[left]"
options:0
metrics:metrics
views:views]];
}
}
if (idx == self.subviews.count -1 ) {
//Place the '0' and delete button
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[zero]-[delete(==zero)]"
options:NSLayoutFormatAlignAllCenterY|NSLayoutFormatAlignAllTop|NSLayoutFormatAlignAllBottom
metrics:metrics
views:@{
@"zero":self.subviews.firstObject,
@"delete":self.subviews.lastObject
}]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[upperCenter]-[zero(==upperCenter)]-|"
options:NSLayoutFormatAlignAllCenterX
metrics:metrics
views:@{
@"zero":self.subviews.firstObject,
@"upperCenter":views[@"center"]
}]];
}
//Make them all square
squareConstraint(self.subviews[idx]);
}
[self addConstraints:constraints];
[super updateConstraints];
}

Monday, July 07, 2014

Future by Design

As a child I loved popular science magazine. Mainly because I was busy designing the future of space propulsion. Hint, it used "controlled" nuclear explosions.

However I've felt that since the 90's we (as a society) have lost our wondering quest for the next generation society. Today I discovered an inventor/futurist named Jacque Fresco. He's been pumping out ideas since before the second world war, some of which I think are brilliant, he also has some interesting views on society, and why it works as it does.