ioskotlinkotlin-multiplatformkotlin-native

Is it possible to observe iOS NSObject value changes with Kotlin/Native


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?


Solution

  • 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?