Implement "toasts" for iOS for UiFeedback.showMessage()
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
package app.klottr.platform
|
||||
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.useContents
|
||||
import platform.Foundation.NSURL
|
||||
@@ -11,14 +12,59 @@ import platform.Foundation.NSURLQueryItem
|
||||
import platform.darwin.dispatch_async
|
||||
import platform.darwin.dispatch_get_main_queue
|
||||
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 {
|
||||
// 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(
|
||||
NSCharacterSet.URLQueryAllowedCharacterSet()
|
||||
)
|
||||
return encoded ?: query
|
||||
) ?: query
|
||||
}
|
||||
|
||||
|
||||
@@ -80,13 +126,88 @@ actual object AppNavigator {
|
||||
"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 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