I am trying to implement an observer for changes to a value for a give key in UserDefaults from the ios native part of a multiplatform project written in Kotlin/Native. Here is the code that I wrote:
fun subscribeForDataChange(storeName: String, callback: () -> Unit) {
NSUserDefaults(storeName).addObserver(
object : NSObject() {
fun observeValue(
observer: NSObject,
forKeyPath: String,
options: NSKeyValueObservingOptions,
context: COpaquePointer?
) {
callback()
print("Data Changed!!!")
}
},
options = NSKeyValueObservingOptionNew,
forKeyPath = DATA_KEY,
context = null
)
}
The problem is that I never get a notification, most probably because the observeValue
is not defined in NSObject, but what else should I do to achieve that?
Here is the solution for 2 apps in the same group sharing UserDefaults. I share SQLite database between two processes and I need to know when one process writes somethink to db. Classical flows are not triggered so I wrote a flow helper, which emit values in Kotlin when NSUserDefaults changes.
Implement NSObject
as a part of the Swift codebase (Swift code inspiration). Swift calls a Kotlin method when NSUserDefaults changes. Firstly define interfaces.
interface NSUserDefaultsKotlinHelper {
fun userDefaultsChanged()
}
interface SwiftInjector {
fun injectIntoSwift(nsUserDefaultsKotlinHelper: NSUserDefaultsKotlinHelper?)
}
Let that interface inject listener into Swift code :
class InterprocessObserver: NSObject, SwiftInjector {
let key: String = "interprocess_communication"
private var nsUserDefaultsKotlinHelper : NSUserDefaultsKotlinHelper?
private let userDefaults = UserDefaults.init(suiteName: "group.your.group.id")
override init() {
super.init()
userDefaults?.addObserver(self, forKeyPath: key, options: [.old, .new], context: nil)
}
func injectIntoSwift(nsUserDefaultsKotlinHelper: NSUserDefaultsKotlinHelper?) {
self.nsUserDefaultsKotlinHelper = nsUserDefaultsKotlinHelper
}
func dataChangedFromAnotherProcess(data : [AnyHashable : Any]) {
userDefaults?.set(data, forKey: key)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let _ = change, object != nil, keyPath == key else { return }
nsUserDefaultsKotlinHelper?.userDefaultsChanged()
}
deinit {
userDefaults?.removeObserver(self, forKeyPath: key, context: nil)
}
}
Inject listener in Kotlin - I will inject when a flow starts to collect:
class InterProcessCommunication(val interPlatformInjector: InterplatformInjector) : InterplatformInjector by interplatformInjector {
val testFlow: Flow<Emitter> = flow {
val channel = Channel<Emitter>(CONFLATED)
channel.trySend(Emitter.STAY_CALM)
val listener = object : IInterprocessCommunication {
override fun interProcessChanged() {
channel.trySend(Emitter.EMIT)
}
}
interPlatformInjector.injectListener(listener)
try {
for (item in channel) {
emit(item)
}
} finally {
interPlatformInjector.injectListener(null)
}
}
}
Objects creation with Koin would be:
//Swift
func initObservers() {
let interplatformInjector = InterprocessObserver()
initKoin(interplatformInjector : interplatformInjector)
}
//Kotlin
fun initKoin(interplatformInjector : InterplatformInjector){
startKoin {
module {
single {InterProcessCommunication(interplatformInjector)}
}
}
}
//Swift Second process (for example NotificationService)
func dataChanged(interprocessObserver : InterprocessObserver) {
interprocessObserver.dataChangedFromAnotherProcess(data) //data could be anythink - for example a string
}
The method dataChenged()
will trigger a Kotlin flow. Is this what you are looking for?