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

2

7

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?

Metts answered 14/5, 2020 at 11:35 Comment(0)
H
1

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?

Heartbreaker answered 25/10, 2022 at 13:10 Comment(0)
D
2

Are you looking for an NSObject change, or you want to observe NSUserDefaults? If the latter, check out Multiplatform Settings. Here's the code that wires up observers.

https://github.com/russhwolf/multiplatform-settings/blob/main/multiplatform-settings/src/appleMain/kotlin/com/russhwolf/settings/NSUserDefaultsSettings.kt

Dent answered 14/5, 2020 at 13:9 Comment(3)
That is indeed very nice workaround and it will solve the problem for most of the use cases when someone needs to observe UserDefaults. But in my case I would need to be able to listen for changes to the UserDefaults from a different process(2 apps in the same group sharing UserDefaults). According to the documentation: developer.apple.com/documentation/foundation/… NSUserDefaultsDidChangeNotification will not fire if the change is done from a different process, therefor I was trying key-value observer.Metts
it now gives 404 error :(Mons
@GeorgeShalvashvili Updated linkDent
H
1

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?

Heartbreaker answered 25/10, 2022 at 13:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.