Skip to content

Instantly share code, notes, and snippets.

@aaronpk
Last active September 20, 2024 23:38
Show Gist options
  • Save aaronpk/0252426d5161bc9650d8 to your computer and use it in GitHub Desktop.
Save aaronpk/0252426d5161bc9650d8 to your computer and use it in GitHub Desktop.
How to compile and install spatialite on iOS

Spatialite for iOS

Install compiler dependencies

brew install automake autoconf libtool libxml2 pkg-config
brew link libxml2

Build libspatialite

git clone https://github.com/gstf/libspatialite-ios.git
cd libspatialite-ios
make

The result is a folder, lib, containing several .a files, as well as an include folder containing many .h files.

Create a new XCode project

Copy the libspatialite binaries and "include" folder into a folder named "libspatialite" in the XCode project folder.

filesystem

In the XCode project's "Build Settings", you'll need to set the library and header search paths:

Library Search Paths: $(PROJECT_DIR)/AppName/libspatialite Header Search Paths: $(PROJECT_DIR)/AppName/libspatialite/include

search paths

Drag the .a files into your project.

From the "Build Phases" window, add the following to the section "Link Binary With Libraries":

  • libz.dylib
  • libxml2.2.dylib
  • libc++.dylib
  • libcharset.1.0.0.dylib
  • libiconv.dylib

XCode Project

Now you should be able to use spatialite! To test if everything worked, just make your AppDelegate output the spatialite version.

Add the following to AppDelegate.m

#include <sqlite3.h>
#include <spatialite/gaiageo.h>
#include <spatialite.h>

In your application:didFinishLaunchingWithOptions: method, add:

spatialite_init (0);
printf("Spatialite version: %s\n", spatialite_version());

Compile and run and you should see the version output in the console!

@Tybion
Copy link

Tybion commented Feb 10, 2024

To follow on from my question - I now have fully working Swift code - that might be useful to someone starting out, like me. It creates a .sqlite file, adds the spatialite metadata structure, creates a table, adds a geometry field to that table, inserts a row (using ST_GeomFromText(), and retrieves that same row to show that it all works. Feel free to add it to any documentation. You can paste it into any new project. I used Xcode 15.2.

import SQLite3
import Spatial
//------other code ----------

// Thank you to Kodeco for the sqlite3 code ..
// https://www.kodeco.com/6620276-sqlite-with-swift-tutorial-getting-started

let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let docsPath = documentsUrl.path  // no trailing slash
let shortname =  "spatialtest6.sqlite"

let fullname = docsPath + "/" + shortname
           
var db: OpaquePointer?
print(shortname)
if sqlite3_open(fullname, &db) != SQLITE_OK {
   print("Unable to open database - \(shortname).")
}

var spconnect: OpaquePointer?
spatialite_initialize()
print("Spatialite version: \(spatialite_version()!)")
spatialite_init_ex(db, &spconnect, 1)

//    spatialite_init_ex((sqlite3 *) sqliteHandle, spatialiteConnection, 0);
let tableName = "features"
let createTableString = """
CREATE TABLE \(tableName) (
id integer PRIMARY KEY,
title text,
descr text);
"""
var stmnt: OpaquePointer?
print(createTableString)
if sqlite3_prepare_v2(db, createTableString, -1, &stmnt, nil) == SQLITE_OK {
    if sqlite3_step(stmnt) == SQLITE_DONE {
      print("Table created.")
    } else {
      print("Table not created.")
    }
} else {
    print("Statement not prepared.")
}

var stmnt4: OpaquePointer?
let sqlStr = "SELECT InitSpatialMetaData('WGS84');"
print(sqlStr)
if sqlite3_prepare_v2(db, sqlStr, -1, &stmnt4, nil) == SQLITE_OK {
    if sqlite3_step(stmnt4) == SQLITE_ROW {
        let result = sqlite3_column_int(stmnt4, 0)
        print("Query Result: \(result)")
    } else {
        print("Query returned no results.")
    }
} else {
    let errorMessage = String(cString: sqlite3_errmsg(db))
    print("Query is not prepared \(errorMessage)")
}

let sqlSpatial = [
        "SELECT AddGeometryColumn('\(tableName)', 'geom', 4326, 'POINT', 'XY');",
        "SELECT CreateSpatialIndex('\(tableName)', 'geom');"]
for sql in sqlSpatial {
    var stmt: OpaquePointer?
    print(sql)
    if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
        if sqlite3_step(stmt) == SQLITE_ROW {
            let result = sqlite3_column_int(stmt, 0)
            print("Query Result: \(result)")
        } else {
            print("Query returned no results.")
        }
    } else {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("Query is not prepared \(errorMessage)")
    }
}

sqlite3_finalize(stmnt)
let insertStr = "INSERT INTO \(tableName) (id, title, descr, geom) " +
        "VALUES ( ?, ?, ?, ST_GeomFromText('POINT(126.0 -34.0)', 4326) );"
print(insertStr)
var stmnt2: OpaquePointer?
if sqlite3_prepare_v2(db, insertStr, -1, &stmnt2, nil) == SQLITE_OK {
    let id: Int32 = 0
    let title: NSString = "Pelican"
    let descr: NSString = "Likes fish"
    sqlite3_bind_int(stmnt2, 1, id)
    sqlite3_bind_text(stmnt2, 2, title.utf8String, -1, nil)
    sqlite3_bind_text(stmnt2, 3, descr.utf8String, -1, nil)
    if sqlite3_step(stmnt2) == SQLITE_DONE {
        print("Inserted row.")
    } else {
      print("Could not insert row.")
    }
} else {
    print("Insert statement not prepared.")
}
sqlite3_finalize(stmnt2)

var queryStr = "SELECT id, title, descr, ST_AsText(geom) AS wkt FROM features;"
print(queryStr)
var stmnt3: OpaquePointer?
if sqlite3_prepare_v2(db, queryStr, -1, &stmnt3, nil) == SQLITE_OK {
    if sqlite3_step(stmnt3) == SQLITE_ROW {
        let id = sqlite3_column_int(stmnt3, 0)
        guard let result = sqlite3_column_text(stmnt3, 1) else {
            print("Query result is nil")
            return
        }
        let title = String(cString: result)
        let descr = String(cString: sqlite3_column_text(stmnt3, 2))
        let wkt = String(cString: sqlite3_column_text(stmnt3, 3))
        print("Query Result: \(id), \(title), \(descr), \(wkt)")
    } else {
        print("Query returned no results.")
    }
} else {
    let errorMessage = String(cString: sqlite3_errmsg(db))
    print("Query is not prepared \(errorMessage)")
}
sqlite3_finalize(stmnt3) 

@Tybion
Copy link

Tybion commented Feb 11, 2024

The same steps, but using the GRDB wrapper - https://github.com/groue/GRDB.swift
Late edit: this worked fine until an upgrade. I wasn't able to resolve the problem so needed to discard GRDB.

import SQLite3
import Spatial
import GRDB

// ------- other code -------

let fileManager = FileManager.default
let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let docsPath = documentsUrl.path  // no trailing slash
let shortname =  "spatialtest.sqlite"
let tablename = shortname.replacingOccurrences(of: ".sqlite", with: "").lowercased()
let fullname = docsPath + "/" + shortname
   
do {
   print("Delete \(shortname)")
   try fileManager.removeItem(atPath: fullname)

   print("Create .sqlite file, initialize spatialite and connect to the database")
   let dbQueue = try DatabaseQueue(path: fullname)
   try dbQueue.inDatabase { db in
       spatialite_initialize()
       var spconnect: OpaquePointer?
       spatialite_init_ex(db.sqliteConnection, &spconnect, 1)
      
       print("Create \(tablename) table")
       try db.create(table: tablename, ifNotExists: true) { t in
           t.autoIncrementedPrimaryKey("ogc_fid")
           t.column("title", .text)
           t.column("descr", .text)
       }
       
       print("Create spatialite metadata structure")
       try db.execute(literal: "SELECT InitSpatialMetaData('WGS84');")
       
       print("Add POINT geometry field & create a spatial index for it")
       let sqlSpatial = [
               "SELECT AddGeometryColumn('\(tablename)', 'geom', 4326, 'POINT', 'XY');",
               "SELECT CreateSpatialIndex('\(tablename)', 'geom');"]
       for sql in sqlSpatial {
           try db.execute(sql: sql)
       }
       
       let insertStr = "INSERT INTO \(tablename) (title, descr, geom) " +
               "VALUES (?, ?, ST_GeomFromText('POINT(126.0 -34.0)', 4326) );"
       print(insertStr)
       try db.execute(sql: insertStr, arguments: ["Pelican", "Thermaling up high."])

       let sqlStr = "SELECT ogc_fid, title, descr, ST_AsText(geom) AS wkt FROM \(tablename);"
       print(sqlStr)
       let rows = try Row.fetchCursor(db, sql:sqlStr, arguments: [])
       while let row = try rows.next() {
           let id = row["ogc_fid"]
           let title = row["title"]
           let descr = row["descr"]
           let wkt = row["wkt"]
           print("ogc_fid=\(id!), title=\(title!), desc=\(descr!), geom=\(wkt!)")
       }
       
   }
} catch let error as NSError {
   let str = "\(error.debugDescription)"
   print(str)
   return
}
      

@ali711
Copy link

ali711 commented Mar 20, 2024

@smbkr @mrclayman i have seen both of you guys have worked on building spatialite i tried both of the solutions.
I built spatialite 4.4.0 using https://github.com/smbkr/libspatialite-ios/blob/master/Makefile and
Last i used was https://github.com/mrclayman/libspatialite-ios/blob/libspatialite-5/Makefile

In both of the solutions base spatialite functions works fine in IOS but functions which require geos or proj or rtree does not work.(i also built another with rttopo for test purpose by manually disabeling GEOSContext_setErrorMessageHandler_r() check and it was built successfully)
For example ST_IsValid , IsValidReason , ST_Within , ST_Overlaps etc. ST_Within does not filter any feature. ST_IsValid always returns -1 even when i tried it on SELECT ST_IsValid(ST_GeomFromText('POINT(15.785 21.713)')).
I believe there is some library communication problem or might need to update build process.
I have posted complete problem detail here as well
https://stackoverflow.com/questions/78187601/st-within-st-isvalid-or-any-other-geos-or-rtreetopo-related-function-not-working.

@vdshko
Copy link

vdshko commented Sep 12, 2024

@ali711 Hi) Have you figured this out?! I faced with the same issue( This is my code:

func my() {
        let dbName: String = "country_boundaries123.db"
        let docsPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.path
        let fullname = docsPath + "/" + dbName

        do {
            let dbQueue = try DatabaseQueue(path: fullname)
            try dbQueue.inDatabase { db in
                spatialite_initialize()
                var spconnect = spatialite_alloc_connection()
                spatialite_init_ex(db.sqliteConnection, &spconnect, 1)
                print("Before execute")
                let query: String = """
                SELECT boundaries.alpha2Code FROM boundaries WHERE ST_Within(ST_PointFromText('POINT(25.276496038 54.684885945)'), geometry)==1;
                """
                let row = try Row.fetchOne(db, sql: query)
                print(row)
                print("After execute")
            }
        } catch let error as NSError {
            let str = "\(error.debugDescription)"
            print(str)
            return
        }
    }

Row.fetchOne, Row.fetchAll, Row.fetchCursor - all of this didn't give me any results. There are no errors, that can be caught in "do{} catch{}" block. So it looks like these functions for Geos and Proj are present but do nothing.

@Tybion You write code-snippets above, maybe you can know what's wrong?
I'm stuck here and need some help please, from anybody. Any ideas?

@Tybion
Copy link

Tybion commented Sep 12, 2024

Sorry, after upgrading GRDB my code stopped working, and I had to fix it quickly, so I reverted my code to the sqlite3 code that I have listed above.

@vdshko
Copy link

vdshko commented Sep 12, 2024

@Tybion Do you still have these generated libs? Can you share it, please? Maybe, if I can compare yours and mine I will find the solution.

@Tybion
Copy link

Tybion commented Sep 12, 2024

I am travelling, only have a phone, but my working code was based completely on the sample code above.

@vdshko
Copy link

vdshko commented Sep 20, 2024

It took some time to find a solution, but it was worth it.
I wrote an article with explanations to help developers like us avoid the same struggle.
Long story short - for proper work SpatiaLite needs to be initialized on the C level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment