@momo-kits/native-kits 0.160.1-scrolltotop.15-debug → 0.160.1-scrolltotop.17-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.160.1-scrolltotop.15-debug"
43
+ version = "0.160.1-scrolltotop.17-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.159.1-beta.12'
3
+ spec.version = '0.160.1-scrolltotop.15'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -6,6 +6,3 @@ import androidx.compose.runtime.Composable
6
6
  internal actual fun RegisterScrollToTop(navigator: Navigator, callback: (() -> Unit)?) {
7
7
  // No-op on Android: status bar tap doesn't trigger scroll-to-top on this platform.
8
8
  }
9
-
10
- actual fun onNavigatorEntered(navigator: Navigator) = Unit
11
- actual fun onNavigatorExited(navigator: Navigator) = Unit
@@ -216,6 +216,10 @@ object DynamicScreenRegistry {
216
216
  fun setOptions(id: Int, options: NavigationOptions){
217
217
  screens[id]?.options = options
218
218
  }
219
+
220
+ fun scrollToTop() {
221
+ getLatestScreen()?.options?.scrollData?.scrollToTopCallback?.invoke()
222
+ }
219
223
  }
220
224
 
221
225
 
@@ -4,15 +4,3 @@ import androidx.compose.runtime.Composable
4
4
 
5
5
  @Composable
6
6
  internal expect fun RegisterScrollToTop(navigator: Navigator, callback: (() -> Unit)?)
7
-
8
- /**
9
- * Call when a Navigator becomes the active (visually topmost) one — e.g. from
10
- * `ComposeNavigatorManager.push`. Android: no-op.
11
- */
12
- expect fun onNavigatorEntered(navigator: Navigator)
13
-
14
- /**
15
- * Call when a Navigator is no longer active — e.g. from
16
- * `ComposeNavigatorManager.removeLast`. Android: no-op.
17
- */
18
- expect fun onNavigatorExited(navigator: Navigator)
@@ -3,9 +3,129 @@ package vn.momo.kits.navigation
3
3
  import androidx.compose.runtime.Composable
4
4
  import androidx.compose.runtime.DisposableEffect
5
5
  import androidx.compose.runtime.remember
6
+ import kotlinx.cinterop.BetaInteropApi
7
+ import kotlinx.cinterop.CFunction
8
+ import kotlinx.cinterop.COpaquePointer
9
+ import kotlinx.cinterop.CPointer
10
+ import kotlinx.cinterop.ExperimentalForeignApi
11
+ import kotlinx.cinterop.ObjCClass
12
+ import kotlinx.cinterop.reinterpret
13
+ import kotlinx.cinterop.staticCFunction
14
+ import kotlinx.cinterop.toKString
15
+ import platform.Foundation.NSNotification
16
+ import platform.Foundation.NSNotificationCenter
17
+ import platform.Foundation.NSOperationQueue
18
+ import platform.darwin.NSObjectProtocol
19
+ import platform.objc.class_getInstanceMethod
20
+ import platform.objc.class_replaceMethod
21
+ import platform.objc.method_getImplementation
22
+ import platform.objc.method_getTypeEncoding
23
+ import platform.objc.method_setImplementation
24
+ import platform.objc.objc_getClass
25
+ import platform.objc.sel_registerName
6
26
  import kotlin.time.Clock
7
27
  import kotlin.time.ExperimentalTime
8
28
 
29
+ @OptIn(ExperimentalForeignApi::class)
30
+ private val replacementHandleTap = staticCFunction<COpaquePointer, COpaquePointer, COpaquePointer?, Unit> { _, _, _ ->
31
+ NSNotificationCenter.defaultCenter.postNotificationName("statusBarSelected", null)
32
+ }
33
+
34
+ @OptIn(ExperimentalForeignApi::class)
35
+ private var originalHandleTapImp: CPointer<CFunction<() -> Unit>>? = null
36
+
37
+ private var swizzleInstalled = false
38
+
39
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
40
+ private fun installStatusBarSwizzle() {
41
+ try {
42
+ if (swizzleInstalled) return
43
+ // objc_getClass returns Any? in the binding, but the underlying value is
44
+ // the ObjC Class metaobject — safe to cast to ObjCClass for the other
45
+ // runtime calls that require it.
46
+ val cls = objc_getClass("UIStatusBarManager") as? ObjCClass ?: run {
47
+ log("swizzle skipped: UIStatusBarManager class not found")
48
+ return
49
+ }
50
+ val selector = sel_registerName("handleTapAction:") ?: run {
51
+ log("swizzle skipped: handleTapAction: selector unavailable")
52
+ return
53
+ }
54
+ val method = class_getInstanceMethod(cls, selector) ?: run {
55
+ log("swizzle skipped: handleTapAction: method not found")
56
+ return
57
+ }
58
+ // Capture original IMP so we can restore it on uninstall.
59
+ originalHandleTapImp = method_getImplementation(method)
60
+ val typeEncoding = method_getTypeEncoding(method)?.toKString()
61
+ class_replaceMethod(cls, selector, replacementHandleTap.reinterpret(), typeEncoding)
62
+ swizzleInstalled = true
63
+ log("UIStatusBarManager handleTapAction: replaced (originalImp captured)")
64
+ } catch (e: Throwable) {
65
+ log("install error: ${e.message}")
66
+ }
67
+ }
68
+
69
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
70
+ private fun uninstallStatusBarSwizzle() {
71
+ try {
72
+ if (!swizzleInstalled) return
73
+ val original = originalHandleTapImp ?: run {
74
+ log("uninstall skipped: no original IMP captured")
75
+ return
76
+ }
77
+ val cls = objc_getClass("UIStatusBarManager") as? ObjCClass ?: return
78
+ val selector = sel_registerName("handleTapAction:") ?: return
79
+ val method = class_getInstanceMethod(cls, selector) ?: return
80
+ method_setImplementation(method, original)
81
+ originalHandleTapImp = null
82
+ swizzleInstalled = false
83
+ log("UIStatusBarManager handleTapAction: original IMP restored")
84
+ } catch (e: Throwable) {
85
+ log("uninstall error: ${e.message}")
86
+ }
87
+ }
88
+
89
+
90
+ private object StatusBarObserver {
91
+ private var token: NSObjectProtocol? = null
92
+ private var refCount = 0
93
+
94
+ fun acquire() {
95
+ refCount++
96
+ if (token != null) {
97
+ log("acquire (already installed) refCount=$refCount")
98
+ return
99
+ }
100
+ installStatusBarSwizzle()
101
+ token = NSNotificationCenter.defaultCenter.addObserverForName(
102
+ name = "statusBarSelected",
103
+ `object` = null,
104
+ queue = NSOperationQueue.mainQueue,
105
+ ) { _: NSNotification? ->
106
+ log("statusBar tap notification → ScrollToTopRegistry.trigger()")
107
+ DynamicScreenRegistry.scrollToTop()
108
+ }
109
+ log("StatusBarObserver installed refCount=$refCount")
110
+ }
111
+
112
+ fun release() {
113
+ if (refCount <= 0) {
114
+ log("release ignored (refCount already 0)")
115
+ return
116
+ }
117
+ refCount--
118
+ if (refCount > 0) {
119
+ log("release refCount=$refCount")
120
+ return
121
+ }
122
+ token?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }
123
+ token = null
124
+ uninstallStatusBarSwizzle()
125
+ log("StatusBarObserver uninstalled refCount=0")
126
+ }
127
+ }
128
+
9
129
  private const val TAG = "[ScrollToTop]"
10
130
 
11
131
  private fun Any.tag(): String = "${this::class.simpleName ?: "Any"}@${hashCode().toString(16)}"
@@ -23,102 +143,76 @@ private fun ts(): String {
23
143
 
24
144
  private fun log(msg: String) = println("${ts()} $TAG $msg")
25
145
 
26
- actual fun onNavigatorEntered(navigator: Navigator) {
27
- log("onNavigatorEntered owner=${navigator.tag()}")
28
- ScrollToTopRegistry.pushOwner(navigator)
29
- }
30
146
 
31
- actual fun onNavigatorExited(navigator: Navigator) {
32
- log("onNavigatorExited owner=${navigator.tag()}")
33
- ScrollToTopRegistry.removeOwner(navigator)
34
- }
35
147
 
36
148
  @Composable
37
149
  internal actual fun RegisterScrollToTop(navigator: Navigator, callback: (() -> Unit)?) {
38
150
  val token = remember { Any() }
39
151
  DisposableEffect(navigator, callback) {
40
- if (callback != null) {
152
+ val acquired = callback != null
153
+ if (acquired) {
154
+ StatusBarObserver.acquire()
41
155
  log("RegisterScrollToTop push owner=${navigator.tag()} token=${token.tag()}")
42
- ScrollToTopRegistry.push(navigator, token, callback)
156
+ // ScrollToTopRegistry.push(navigator, token, callback)
43
157
  } else {
44
158
  log("RegisterScrollToTop remove(callback=null) owner=${navigator.tag()} token=${token.tag()}")
45
- ScrollToTopRegistry.remove(navigator, token)
159
+ // ScrollToTopRegistry.remove(navigator, token)
46
160
  }
47
161
  onDispose {
48
162
  log("RegisterScrollToTop dispose owner=${navigator.tag()} token=${token.tag()}")
49
- ScrollToTopRegistry.remove(navigator, token)
163
+ // ScrollToTopRegistry.remove(navigator, token)
164
+ if (acquired) StatusBarObserver.release()
50
165
  }
51
166
  }
52
167
  }
53
168
 
54
- /**
55
- * iOS-only scroll-to-top dispatch.
56
- *
57
- * Per-Navigator stack of screen callbacks, plus a module-level stack of owners
58
- * (Navigators). The host project's Navigator-of-Navigators (e.g.
59
- * `ComposeNavigatorManager`) must call [pushOwner] / [removeOwner] when its
60
- * top Navigator changes, so [trigger] hits the visually topmost screen.
61
- *
62
- * Call [trigger] from Swift on status-bar tap.
63
- */
64
- object ScrollToTopRegistry {
65
- private data class Entry(val token: Any, val callback: () -> Unit)
66
-
67
- private val stacksByOwner = mutableMapOf<Any, MutableList<Entry>>()
68
- private val ownerStack = mutableListOf<Any>()
69
-
70
- fun pushOwner(owner: Any) {
71
- val existed = ownerStack.remove(owner)
72
- ownerStack.add(owner)
73
- log("pushOwner owner=${owner.tag()} reorder=$existed ownerStack=${ownerStack.size} state=${dump()}")
74
- }
75
-
76
- fun removeOwner(owner: Any) {
77
- val removedFromStack = ownerStack.remove(owner)
78
- val removedScreens = stacksByOwner.remove(owner)?.size ?: 0
79
- log("removeOwner owner=${owner.tag()} removedFromStack=$removedFromStack droppedScreens=$removedScreens state=${dump()}")
80
- }
81
-
82
- internal fun push(owner: Any, token: Any, callback: () -> Unit) {
83
- val stack = stacksByOwner.getOrPut(owner) { mutableListOf() }
84
- val idx = stack.indexOfFirst { it.token === token }
85
- if (idx >= 0) {
86
- stack[idx] = Entry(token, callback)
87
- log("push(update) owner=${owner.tag()} token=${token.tag()} idx=$idx size=${stack.size} state=${dump()}")
88
- } else {
89
- stack.add(Entry(token, callback))
90
- log("push(new) owner=${owner.tag()} token=${token.tag()} size=${stack.size} state=${dump()}")
91
- }
92
- }
93
-
94
- internal fun remove(owner: Any, token: Any) {
95
- val stack = stacksByOwner[owner]
96
- val before = stack?.size ?: 0
97
- val removed = stack?.removeAll { it.token === token } ?: false
98
- log("remove owner=${owner.tag()} token=${token.tag()} removed=$removed size=${before}->${stack?.size ?: 0} state=${dump()}")
99
- }
100
-
101
- fun trigger() {
102
- val owner = ownerStack.lastOrNull()
103
- if (owner == null) {
104
- if (stacksByOwner.size == 1) {
105
- val only = stacksByOwner.entries.first()
106
- val cb = only.value.lastOrNull()?.callback
107
- log("trigger(fallback singleOwner) owner=${only.key.tag()} hasCallback=${cb != null}")
108
- cb?.invoke()
109
- } else {
110
- log("trigger NO-OP (no owner, owners=${stacksByOwner.size}) state=${dump()}")
111
- }
112
- return
113
- }
114
- val cb = stacksByOwner[owner]?.lastOrNull()?.callback
115
- log("trigger owner=${owner.tag()} hasCallback=${cb != null} state=${dump()}")
116
- cb?.invoke()
117
- }
118
-
119
- private fun dump(): String {
120
- val owners = ownerStack.joinToString(",") { it.tag() }
121
- val stacks = stacksByOwner.entries.joinToString(",") { (o, s) -> "${o.tag()}:${s.size}" }
122
- return "ownerStack=[$owners] stacks={$stacks}"
123
- }
124
- }
169
+ //object ScrollToTopRegistry {
170
+ // private data class Entry(val token: Any, val callback: () -> Unit)
171
+ //
172
+ // private val stacksByOwner = mutableMapOf<Any, MutableList<Entry>>()
173
+ // private val ownerStack = mutableListOf<Any>()
174
+ //
175
+ //
176
+ // internal fun push(owner: Any, token: Any, callback: () -> Unit) {
177
+ // val stack = stacksByOwner.getOrPut(owner) { mutableListOf() }
178
+ // val idx = stack.indexOfFirst { it.token === token }
179
+ // if (idx >= 0) {
180
+ // stack[idx] = Entry(token, callback)
181
+ // log("push(update) owner=${owner.tag()} token=${token.tag()} idx=$idx size=${stack.size} state=${dump()}")
182
+ // } else {
183
+ // stack.add(Entry(token, callback))
184
+ // log("push(new) owner=${owner.tag()} token=${token.tag()} size=${stack.size} state=${dump()}")
185
+ // }
186
+ // }
187
+ //
188
+ // internal fun remove(owner: Any, token: Any) {
189
+ // val stack = stacksByOwner[owner]
190
+ // val before = stack?.size ?: 0
191
+ // val removed = stack?.removeAll { it.token === token } ?: false
192
+ // log("remove owner=${owner.tag()} token=${token.tag()} removed=$removed size=${before}->${stack?.size ?: 0} state=${dump()}")
193
+ // }
194
+ //
195
+ // fun trigger() {
196
+ // val owner = ownerStack.lastOrNull()
197
+ // if (owner == null) {
198
+ // if (stacksByOwner.size == 1) {
199
+ // val only = stacksByOwner.entries.first()
200
+ // val cb = only.value.lastOrNull()?.callback
201
+ // log("trigger(fallback singleOwner) owner=${only.key.tag()} hasCallback=${cb != null}")
202
+ // cb?.invoke()
203
+ // } else {
204
+ // log("trigger NO-OP (no owner, owners=${stacksByOwner.size}) state=${dump()}")
205
+ // }
206
+ // return
207
+ // }
208
+ // val cb = stacksByOwner[owner]?.lastOrNull()?.callback
209
+ // log("trigger owner=${owner.tag()} hasCallback=${cb != null} state=${dump()}")
210
+ // cb?.invoke()
211
+ // }
212
+ //
213
+ // private fun dump(): String {
214
+ // val owners = ownerStack.joinToString(",") { it.tag() }
215
+ // val stacks = stacksByOwner.entries.joinToString(",") { (o, s) -> "${o.tag()}:${s.size}" }
216
+ // return "ownerStack=[$owners] stacks={$stacks}"
217
+ // }
218
+ //}
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.160.1-scrolltotop.15
21
+ version=0.160.1-scrolltotop.17
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/native-kits",
3
- "version": "0.160.1-scrolltotop.15-debug",
3
+ "version": "0.160.1-scrolltotop.17-debug",
4
4
  "private": false,
5
5
  "dependencies": {},
6
6
  "devDependencies": {},
@@ -1,94 +0,0 @@
1
- //
2
- // ScrollToTop.swift
3
- //
4
- //
5
- // Created by Thành Lê on 26/5/26.
6
- //
7
- import Foundation
8
- import SwiftUI
9
- import Combine
10
- extension UIStatusBarManager {
11
- public static var statusBarTappedNotification: Notification.Name = {
12
- if let originalMethod = class_getInstanceMethod(UIStatusBarManager.self, Selector(("handleTapAction:"))),
13
- let swizzledMethod = class_getInstanceMethod(UIStatusBarManager.self, #selector(_handleTapAction)) {
14
- method_exchangeImplementations(originalMethod, swizzledMethod)
15
- }
16
- return .init("statusBarSelected")
17
- }()
18
-
19
- @objc private func _handleTapAction(_ action: Any?) {
20
- _handleTapAction(action) // Call the original implementation
21
- NotificationCenter.default.post(name: UIStatusBarManager.statusBarTappedNotification, object: nil)
22
- }
23
- }
24
-
25
- public struct StatusBarTapModifier: ViewModifier {
26
-
27
- let action: () -> Void
28
-
29
- @State private var observer: AnyCancellable?
30
-
31
- public init(action: @escaping () -> Void) {
32
- self.action = action
33
- }
34
-
35
- public func body(content: Content) -> some View {
36
- content
37
- .onAppear {
38
- observer = NotificationCenter.default
39
- .publisher(for: UIStatusBarManager.statusBarTappedNotification)
40
- .sink { _ in
41
- action()
42
- }
43
- }
44
- .onDisappear {
45
- observer = nil
46
- }
47
- }
48
- }
49
-
50
- extension View {
51
- public func onStatusBarTap(_ action: @escaping () -> Void) -> some View {
52
- modifier(StatusBarTapModifier(action: action))
53
- }
54
- }
55
-
56
- extension UIViewController {
57
-
58
- private static var statusBarTapActionKey: UInt8 = 0
59
- private static var statusBarTapObserverKey: UInt8 = 0
60
-
61
- private var statusBarTapAction: (() -> Void)? {
62
- get { objc_getAssociatedObject(self, &Self.statusBarTapActionKey) as? () -> Void }
63
- set { objc_setAssociatedObject(self, &Self.statusBarTapActionKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) }
64
- }
65
-
66
- private var statusBarTapObserver: NSObjectProtocol? {
67
- get { objc_getAssociatedObject(self, &Self.statusBarTapObserverKey) as? NSObjectProtocol }
68
- set { objc_setAssociatedObject(self, &Self.statusBarTapObserverKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
69
- }
70
-
71
- /// Register a callback fired when the user taps the status bar / notch.
72
- /// Host app wires `action` to its scroll-to-top bridge (e.g. Navigation.scrollData.scrollToTopCallback).
73
- public func registerStatusBarTap(_ action: @escaping () -> Void) {
74
- unregisterStatusBarTap()
75
- statusBarTapAction = action
76
- statusBarTapObserver = NotificationCenter.default.addObserver(
77
- forName: UIStatusBarManager.statusBarTappedNotification,
78
- object: nil,
79
- queue: .main
80
- ) { [weak self] _ in
81
- self?.statusBarTapAction?()
82
- }
83
- }
84
-
85
- public func unregisterStatusBarTap() {
86
- if let observer = statusBarTapObserver {
87
- NotificationCenter.default.removeObserver(observer)
88
- }
89
- statusBarTapObserver = nil
90
- statusBarTapAction = nil
91
- }
92
- }
93
-
94
-