Using Go on iOS in 2021

- go ios objc

I’ve already posted about gomobile on this blog: “Using gomobile for real”.

For a lot of people it seems crazy to run Go on iOS and Android for some others it’s just routine, Zenly & Tailscale to name a few.

Since I’m reviving the idea for a geo database on iPhone here are some details.

I’ve started to port spatialite with the required dependencies to realize most of them were LGPL and incompatible with iOS distribution, so I went back to a simple solution using Go to display points on a map from a local database.

Glue Code

I have a “regular” key value Geo aware database based on bbolt and s2, because gomobile is limited to a subset of types, we have to encapsulate it in another package that gomobile can handle.

So for example my regular Go code is taking a bbolt db as parameter, but from the iOS point of view we just want to pass a path to open a new bbolt db:

func NewGeoDB(db *bbolt.DB) (*GeoDB, error)

Then in a package for gomobile we hide most of the work:

type GeoDB struct {
	idx *index.GeoDB
}

func Open(path string) (*GeoDB, error) 

We hide the index package to iOS by encapsulating it.

Getting data out of the database, because gomobile can’t translate into Go slices we have to be creative []byte is ok:

In regular Go world:

func (g *GeoDB) RectPointQuery(urlat, urlng, bllat, bllng float64) []PointKey

Then in gomobile, we call the same API but we rely on Protocol buffer (see later below) as pivot format, we will then read those entries back from iOS.

func (g *GeoDB) RectPointQueryPB(urlat, urlng, bllat, bllng float64) ([]byte, error) {
	keys := g.idx.RectPointQuery(urlat, urlng, bllat, bllng)
    
	fc := &FeatureCollection{}

	fc.Features = make([]*Feature, len(keys))
	for i, key := range keys {
		lat, lng, mask, dataKey := index.PointKeySplit(key)
		fc.Features[i] = &Feature{
			Id:   int64(dataKey),
			Mask: mask,
			Geometry: &Geometry{
				Type:        Geometry_POINT,
				Coordinates: []float64{lng, lat},
			},
		}
	}

	return proto.Marshal(fc)
}

Note that we are only using informations stored in the keys for listing the points, this is way faster than reading and decoding the values, the properties will be fetched later by a user event asking for details.

To properly display the point by its “kind” (in this case tree breed), we are using a uint32 bitmask to store some informations in the key:

// KeySet is used with the point mask
type KeySet uint32

const (
	KeySet1 KeySet = 1 << iota
    ...

The same mask on iOS:

    if (self.mask & KeySet2)
    {
        return NSLocalizedString(@"PINE", @"Pine");
    }

Rect query on iOS (sorry still using good old Objective-C for this project, but same applies for Swift also note the Swift protocol buffer implementation from Apple) and read the entries back:

- (NSArray*)rectPointQueryPB:(double)urlat urlng:(double)urlng bllat:(double)bllat bllng:(double)bllng error:(NSError *)error {
    NSData *value = [_db rectPointQueryPB:urlat urlng:urlng bllat:bllat bllng:bllng error:&error];
   ...
    NBFeatureCollection *fc = [[NBFeatureCollection alloc] initWithData:value error:&error];
   ...
    
    NSMutableArray *res = [NSMutableArray array];
    for (NBFeature *f in fc.featuresArray) {
        NBPoint *p = [[NBPoint alloc] init];
        p.coordinate = CLLocationCoordinate2DMake([f.geometry.coordinatesArray valueAtIndex:1],[f.geometry.coordinatesArray valueAtIndex:0]);
        p.pid = f.id_p;
        p.mask = f.mask;
        [res addObject:p];
    }
    
    return res;
}

NBPoint is an @interface NBPoint : MKPointAnnotation so we can pass it directly to an MKMapView as an annotation.

Protocol buffer is more performant but requires you to describe your model in advance, CBOR is a good alternative when you have unstructured data since it can be treated like a JSON dict.
Here is the example used proto description:

message Geometry {
    Type type = 1;

    repeated Geometry geometries = 2;

    repeated double coordinates = 3;

    enum Type {
        POINT = 0;
        POLYGON = 1;
        MULTIPOLYGON = 2;
        LINESTRING = 3;
    }
}

message Feature {
    int64 id = 1;
    
    uint32 mask = 2;

    Geometry geometry = 3;

    map<string, google.protobuf.Value> properties = 4;
}

message FeatureCollection {
    repeated Feature features = 1;
}

Generating xcframework

Since Apple transitioned to arm64 with the M1, they needed a new way to package framework to avoid collision with arm64 for the phones, for the M1 simulator and for the Mac itself (maccatalyst).
Gomobile has been udpated recently to switch to the new format:

gomobile bind -target=ios,iossimulator,macos,maccatalyst -o DemoGeoDB/DemoGeoDB/Mobilegeodb.xcframework .	

Resulting application: