Implement "toasts" for iOS for UiFeedback.showMessage()
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package app.klottr.platform
|
package app.klottr.platform
|
||||||
|
|
||||||
|
import kotlinx.cinterop.BetaInteropApi
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
import kotlinx.cinterop.useContents
|
import kotlinx.cinterop.useContents
|
||||||
import platform.Foundation.NSURL
|
import platform.Foundation.NSURL
|
||||||
@@ -11,14 +12,59 @@ import platform.Foundation.NSURLQueryItem
|
|||||||
import platform.darwin.dispatch_async
|
import platform.darwin.dispatch_async
|
||||||
import platform.darwin.dispatch_get_main_queue
|
import platform.darwin.dispatch_get_main_queue
|
||||||
import platform.Foundation.*
|
import platform.Foundation.*
|
||||||
|
import platform.Foundation.NSTimeInterval
|
||||||
|
import platform.UIKit.*
|
||||||
|
import platform.darwin.dispatch_after
|
||||||
|
import platform.darwin.dispatch_time
|
||||||
|
|
||||||
|
private fun runOnMain(block: () -> Unit) {
|
||||||
|
if (NSThread.isMainThread) block() else {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun activeWindow(): UIWindow? {
|
||||||
|
val app = UIApplication.sharedApplication
|
||||||
|
|
||||||
|
// iOS 13+ multi-scene: iterate foreground-active scenes
|
||||||
|
val scenes = app.connectedScenes // NSSet?
|
||||||
|
for (sceneAny in scenes) { // fast-enumeration over NSSet
|
||||||
|
val scene = sceneAny as? UIScene ?: continue
|
||||||
|
if (scene.activationState != UISceneActivationStateForegroundActive) continue
|
||||||
|
|
||||||
|
val windowScene = scene as? UIWindowScene ?: continue
|
||||||
|
val windows = windowScene.windows.mapNotNull { it as? UIWindow }
|
||||||
|
|
||||||
|
// Prefer key window
|
||||||
|
windows.firstOrNull { it.keyWindow }?.let { return it }
|
||||||
|
// Then any visible, normal-level window
|
||||||
|
windows.firstOrNull { !it.isHidden() && it.alpha > 0.0 && it.windowLevel == UIWindowLevelNormal }?.let { return it }
|
||||||
|
// Finally, any window in that scene
|
||||||
|
windows.firstOrNull()?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (pre-iOS 13 or no active scene): scan application windows
|
||||||
|
val appWindows = app.windows.mapNotNull { it as? UIWindow }
|
||||||
|
appWindows.firstOrNull { it.keyWindow }?.let { return it }
|
||||||
|
return appWindows.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun seconds(duration: UiFeedbackDuration): NSTimeInterval =
|
||||||
|
when (duration) {
|
||||||
|
UiFeedbackDuration.SHORT -> 2.0
|
||||||
|
UiFeedbackDuration.LONG -> 3.5
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(BetaInteropApi::class)
|
||||||
private fun urlEncode(query: String): String {
|
private fun urlEncode(query: String): String {
|
||||||
// Percent-encode for use in URL query
|
// Percent-encode for use in URL query
|
||||||
val encoded = (query as NSString)
|
// Create an NSString instance; avoid `as NSString`
|
||||||
|
val nsQuery: NSString = NSString.create(string = query)
|
||||||
|
return nsQuery
|
||||||
.stringByAddingPercentEncodingWithAllowedCharacters(
|
.stringByAddingPercentEncodingWithAllowedCharacters(
|
||||||
NSCharacterSet.URLQueryAllowedCharacterSet()
|
NSCharacterSet.URLQueryAllowedCharacterSet()
|
||||||
)
|
) ?: query
|
||||||
return encoded ?: query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -80,13 +126,88 @@ actual object AppNavigator {
|
|||||||
"maps://?daddr=$encoded&dirflg=d"
|
"maps://?daddr=$encoded&dirflg=d"
|
||||||
}
|
}
|
||||||
|
|
||||||
openUrlOnMain((NSURL(string = urlString)))
|
runOnMain {
|
||||||
|
UIApplication.sharedApplication.openURL(
|
||||||
|
NSURL(string = urlString),
|
||||||
|
options = emptyMap<Any?, Any?>(),
|
||||||
|
completionHandler = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual object UiFeedback {
|
actual object UiFeedback {
|
||||||
actual fun showMessage(text: String, duration: UiFeedbackDuration) {
|
actual fun showMessage(text: String, duration: UiFeedbackDuration) {
|
||||||
// No-op or implement a lightweight HUD; keeping no-op by default.
|
runOnMain {
|
||||||
}
|
val window = activeWindow() ?: return@runOnMain
|
||||||
}
|
|
||||||
|
// Container view (rounded, semi-opaque)
|
||||||
|
val container = UIView().apply {
|
||||||
|
backgroundColor = UIColor.blackColor.colorWithAlphaComponent(0.85)
|
||||||
|
layer.cornerRadius = 12.0
|
||||||
|
clipsToBounds = true
|
||||||
|
alpha = 0.0
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
accessibilityLabel = "Toast"
|
||||||
|
accessibilityTraits = UIAccessibilityTraitStaticText
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
val label = UILabel().apply {
|
||||||
|
textColor = UIColor.whiteColor
|
||||||
|
font = UIFont.systemFontOfSize(15.0)
|
||||||
|
numberOfLines = 0
|
||||||
|
textAlignment = NSTextAlignmentCenter
|
||||||
|
this.text = text
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
setContentCompressionResistancePriority(UILayoutPriorityRequired, UILayoutConstraintAxisHorizontal)
|
||||||
|
setContentHuggingPriority(UILayoutPriorityRequired, UILayoutConstraintAxisHorizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addSubview(label)
|
||||||
|
window.addSubview(container)
|
||||||
|
|
||||||
|
// Layout: centerX, above bottom safe area; label has padding
|
||||||
|
val safe = window.safeAreaLayoutGuide
|
||||||
|
NSLayoutConstraint.activateConstraints(listOf(
|
||||||
|
container.centerXAnchor.constraintEqualToAnchor(window.centerXAnchor),
|
||||||
|
container.bottomAnchor.constraintEqualToAnchor(safe.bottomAnchor, constant = -64.0),
|
||||||
|
container.leadingAnchor.constraintGreaterThanOrEqualToAnchor(window.leadingAnchor, constant = 20.0),
|
||||||
|
container.trailingAnchor.constraintLessThanOrEqualToAnchor(window.trailingAnchor, constant = -20.0),
|
||||||
|
|
||||||
|
label.topAnchor.constraintEqualToAnchor(container.topAnchor, constant = 10.0),
|
||||||
|
label.bottomAnchor.constraintEqualToAnchor(container.bottomAnchor, constant = -10.0),
|
||||||
|
label.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant = 14.0),
|
||||||
|
label.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor, constant = -14.0)
|
||||||
|
))
|
||||||
|
|
||||||
|
// Also cap max width to ~90% of window
|
||||||
|
container.widthAnchor.constraintLessThanOrEqualToAnchor(
|
||||||
|
window.widthAnchor, multiplier = 0.9
|
||||||
|
).active = true
|
||||||
|
|
||||||
|
// Fade in
|
||||||
|
UIView.animateWithDuration(
|
||||||
|
duration = 0.2,
|
||||||
|
animations = { container.alpha = 1.0 },
|
||||||
|
completion = null
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedule fade out + removal
|
||||||
|
val delay = seconds(duration)
|
||||||
|
dispatch_after(
|
||||||
|
// dispatch_time takes nanoseconds
|
||||||
|
dispatch_time(0u, (delay * 1_000_000_000L).toLong()),
|
||||||
|
dispatch_get_main_queue()
|
||||||
|
) {
|
||||||
|
UIView.animateWithDuration(
|
||||||
|
duration = 0.25,
|
||||||
|
animations = { container.alpha = 0.0 },
|
||||||
|
completion = { _ ->
|
||||||
|
container.removeFromSuperview()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user