A major feature of Outboard, my iOS app written with ClojureScript on React Native, is the ability to add places to your lists from a share extension inside Apple’s Maps, Google Maps, Yelp, or other apps that supply URLs. React Native ships with AsyncStorage, a LocalStorage analog for iOS and Android apps, but on iOS, that data is sandboxed to the app alone. To share between an app and an extension, you need to use App Groups, and roll your own persistence layer to interact with it. In this article, I’ll explain how to do that. You should first have an App Group set up on the developer portal and have an entitlement to it added both your app and its extension; you can follow this guide to get to that point.
The native DataManager
First we’ll implement the native persistence layer that will write keys to
the UserDefaults
object for our app group; I’ll do that in Swift. It’s a
simple class with two functions, one to read and one to write.
Note the @objc
macro calls here; we need to eventually provide an ObjC bridge
for ReactNative to talk to it. Our loadData
function takes a key and a
callback function; that function is what we’ll provide in ClojureScript later
to handle reads. The saveData
function simply writes without any callback. We
haven’t done anything with errors in either function; you should build that in
to handle your specific use case. The aforementioned ObjC bridge goes in
DataManagerBridge.m
:
The ClojureScript layer
The bare code for saving/loading is also two simple functions, mediated by
React Native’s NativeModules
.
A few things to note here:
- You may need to provide externs, or use something like cljs-oops for the
.-DataManager
call under advanced compilation - I’m using
pr-str
andread-string
to ednify the data I’m saving - In real life, I actually use
core.async
inget-item
to avoid callback hell, but I’m providing the minimum viable implementation here
At this point, you should be able to put items into storage from the REPL and retrieve them.
Connecting to Re-Frame
The real power from this approach comes at the final Re-Frame step, where I
save my app-db
to UserDefaults
on every change automatically:
You’ll need to make sure that you initialize your state from
(get-item :saved-app-state (fn [_ val] (dispatch-sync [:initialize-db val]))
in your app startup as well.
Now, every time I call (dispatch [:add-something :something])
, that change
to my app-db
will also immediately be persisted locally; any changes made
from the share extension will reflect in the app and vice-versa.
Conclusion
Setting up app groups and persisting to UserDefaults
can be a fiddly
experience in XCode, but the actual code to do so is straighforward. With
Re-Frame Interceptors
to handle automatically persisting your data in the background for you, you can
treat your Clojurescript-written app and extensions as writing and reading from
the same app-db
.