Introduction
KlugDialog shows the operating system's own dialogs in AIR apps on Android, iOS and macOS: alerts with up to three buttons, dialogs with any number of buttons and text fields, and a progress dialog that your code updates and closes.
Each platform uses its native component: an AlertDialog on Android, a UIAlertController on iOS and an NSAlert sheet attached to the app window on macOS. Text fields stay visible above the on-screen keyboard, passwords are never auto-capitalised, and every dialog is asynchronous, so the AIR runtime never blocks while a dialog is open.
| Extension ID | com.klug.dialog.Dialog |
| Current version | 1.2.1 |
| Platforms | Android (arm, arm64, x86, x86_64), iOS (devices, arm64), macOS (Apple silicon and Intel). On every other platform, including Windows and the iOS Simulator, the extension loads, isSupported is false and all calls are safe no-ops. |
| ActionScript API | One class: com.klug.dialog.Dialog |
| Delivery | com.klug.dialog.Dialog.ane (the extension), the ActionScript library (SWC) for code completion, and this documentation |
Requirements
| AIR SDK | 51.x (HARMAN) |
| Android | Android 6 or later |
| iOS | iOS 13 or later, real devices |
| macOS | macOS 11 or later |
| Permissions | None on any platform |
| Signing | No entitlements needed; dialogs also work in unsigned or development-signed local builds |
Add the extension
Copy com.klug.dialog.Dialog.ane into your project's extension folder (the folder you pass to ADT with -extdir, or the one your IDE links as native extensions) and add the extension ID to the application descriptor:
<extensions>
<extensionID>com.klug.dialog.Dialog</extensionID>
</extensions>
That is all. KlugDialog needs no Android manifest additions, no iOS InfoAdditions or Entitlements and no macOS entitlements.
Getting started
Get the instance, check support, show an alert. The call returns at once; the chosen button arrives later in your callback.
import com.klug.dialog.Dialog; var dialog:Dialog = Dialog.getInstance(); // singleton: never call new Dialog() yourself if ( dialog.isSupported ) // Android, iOS and macOS { dialog.show( "Saved", "Your settings were saved." ); // one "OK" button dialog.show( "Delete recording?", "This cannot be undone.", [ "Delete", "Keep" ], function( index:int ):void { if ( index == 0 ) deleteRecording(); // your app function } ); } else { showAppAlert( "Saved" ); // your app's own fallback, e.g. on Windows }
Guides
Simple alerts with show()
show() is the quick way to inform or ask: a title, a message and up to three buttons (default: one "OK" button). The callback receives the position of the chosen button in your array, starting at 0. Buttons beyond the third are ignored.
| Android | Native AlertDialog. The dialog can also be closed with the back key or a tap outside; the callback then receives -1. The style parameter is not used. |
| iOS | UIAlertController alert, all buttons in the default style. The style parameter is not used. In the rare case that the alert cannot be presented, the callback receives -1. |
| macOS | NSAlert sheet attached to the app window, with the app icon. The first button is the default button (Return key). style selects the alert style. |
dialog.show( "Leave the call?", "The other side will be disconnected.", [ "Leave", "Stay" ], function( index:int ):void { if ( index == 0 ) leaveCall(); // your app function // 1 = Stay, -1 = closed without a button (Android back key / tap outside) }, Dialog.STYLE_WARNING ); // macOS alert style; ignored on Android and iOS
macOS sheet styles
On macOS the last parameter of show() sets the NSAlert style of the sheet. All three show your app icon; the critical style adds the system's caution badge.
| Dialog.STYLE_INFO (0) | Informational sheet. The default. |
| Dialog.STYLE_WARNING (1) | Warning sheet. |
| Dialog.STYLE_CRITICAL (2) | Critical sheet, for actions that cannot be undone. |
Sheets slide down from the title bar of the app's main window. If no window is visible yet, for example very early during startup, macOS shows a separate modal alert instead.
Dialogs with tagged buttons: present()
present() takes any number of buttons. Each button is an object {label:String, style:int, tag:int}. The callback receives the tag of the chosen button, so the result does not depend on the order in which a platform lays out the buttons. style is a hint for the platform:
| style | Android · iOS · macOS |
| 0 default | Android: confirming button, right side · iOS: default action · macOS: normal button |
| 1 positive | Android: confirming button, right side · iOS: default action · macOS: normal button |
| 2 negative | Android: left side · iOS: default action · macOS: normal button |
| 3 destructive | Android: red text, left side · iOS: destructive action (red) · macOS: normal button |
| 4 cancel | Android: left side · iOS: cancel action, placed by the system (only the first cancel button gets this style) · macOS: normal button |
| 5 neutral | Android: left side · iOS: default action · macOS: normal button |
Layout on Android: up to two buttons with labels of at most 18 characters sit in one row, with styles 2 to 5 on the left and styles 0 and 1 on the right. With three or more buttons, or a longer label, the buttons are stacked full width in the order of your array; use this for action lists. macOS adds the buttons in array order, and the first one is the default button. iOS places the buttons according to the system's alert rules.
var buttons:Array = [ { label:"Record video", style:0, tag:1 }, { label:"Take a photo", style:0, tag:2 }, { label:"Delete all", style:3, tag:3 }, // destructive { label:"Cancel", style:4, tag:0 } // cancel ]; dialog.present( "Camera", "What would you like to do?", true, buttons, null, function( tag:int, values:Array ):void { switch ( tag ) { case 1: startRecording(); break; // your app functions case 2: takePhoto(); break; case 3: confirmDeleteAll(); break; } } );
Text input fields
Pass an array of field objects as the fifth parameter. The callback receives the entered texts in values, in the order of your fields, together with the tag of the button. Every field object may contain:
| placeholder:String | Hint text shown while the field is empty. All platforms. |
| secure:Boolean | Password field with hidden input. All platforms. Secure fields are never auto-capitalised or auto-corrected. |
| text:String | Initial text, e.g. the last used e-mail address. All platforms. |
| keyboardType:int | Keyboard for the field (table below). Android and iOS. |
| autocorrect:Boolean | Suggestions and auto-correction. Off unless set to true. Android and iOS. |
| autocap:int | Auto-capitalisation: 0 none (default), 1 sentences, 2 words, 3 all characters. Android and iOS. |
| keyboardType | iOS · Android |
| 0 | Default keyboard · text |
| 1 | ASCII-capable keyboard · text |
| 2 | Numbers and punctuation · text |
| 3 | URL keyboard · URL keyboard |
| 4 | Number pad · numbers |
| 5 | Phone pad · phone keyboard |
| 6 | Name and phone pad · phone keyboard |
| 7 | E-mail keyboard · e-mail keyboard |
| 8 | Decimal pad · decimal numbers |
| 9 | Twitter keyboard · text |
| 10 | Web search keyboard · text |
Android: dialogs with fields open near the top of the screen, the first field gets the focus and the keyboard opens; the message and the fields scroll while the buttons stay visible above the keyboard, also in landscape on tablets. The keyboard shows "Next" on each field and "Done" on the last one, and it is closed again when the dialog closes. A secure field with keyboard type 4 or 8 becomes a numeric PIN field. iOS: the system keeps the alert above the keyboard; secure fields are marked as password fields. macOS: the fields are stacked below the message and the first one gets the focus; keyboard type, auto-correction and auto-capitalisation do not apply.
var buttons:Array = [ { label:"Cancel", style:4, tag:0 }, { label:"Sign in", style:1, tag:1 } ]; var fields:Array = [ { placeholder:"E-mail", text:lastEmail, keyboardType:7 }, // lastEmail: your app's value { placeholder:"Password", secure:true } ]; dialog.present( "Sign in", "Please enter your account.", false, buttons, fields, function( tag:int, values:Array ):void { if ( tag != 1 ) return; var email:String = values[0]; var password:String = values[1]; if ( email == "" || password == "" ) showSignInAgain(); // your app else signIn( email, password ); // your app } );
Cancelable dialogs
The third parameter of present(), cancelable, is used on Android: when true, the back key and a tap outside close the dialog without a button. Your callback is then called with tag -1, an empty array and cancelled = true, but only if it declares this third parameter. A callback with two parameters is simply not called in that case.
On iOS and macOS the user always closes a dialog with one of its buttons, so include a cancel button there. On iOS the same cancelled call also happens in the rare case that the dialog cannot be presented at all.
dialog.present( "Rename camera", null, true, [ { label:"Cancel", style:4, tag:0 }, { label:"Save", style:1, tag:1 } ], [ { placeholder:"Name", text:cameraName, autocap:2 } ], // capitalise words function( tag:int, values:Array, cancelled:Boolean = false ):void { if ( cancelled || tag != 1 ) return; // back key, tap outside or Cancel renameCamera( values[0] ); // your app function } );
Close a dialog from code
Pass your own id as the seventh parameter of present(). presentDismiss(id) then closes that dialog; its callback is not called. Use this when the reason for the dialog disappears, for example when the other side cancels a request while your question is still open. On Android and iOS, presenting a new dialog with the same id replaces the open one; use a new id for each dialog that is open at the same time.
private const REQUEST_ID:String = "incomingRequest"; public function onIncomingRequest( from:String ):void { dialog.present( "Video call", from + " would like to start a video call.", false, [ { label:"Decline", style:2, tag:0 }, { label:"Accept", style:1, tag:1 } ], null, function( tag:int, values:Array ):void { if ( tag == 1 ) acceptCall(); else declineCall(); // your app }, REQUEST_ID ); } public function onRequestWithdrawn():void { dialog.presentDismiss( REQUEST_ID ); // closes it; no callback }
Progress dialog
progressShow() opens a dialog with a spinner, a title and a message, and no buttons. You choose its id; with the same id you change the message (progressUpdate) and close it (progressDismiss). The optional closeCallback is called once, when the dialog is really gone. While a progress dialog is open, change its text with progressUpdate() rather than calling progressShow() again with the same id; to show a new one, dismiss the old one first or use a new id.
| Android | Modal dialog with a spinner and the message beside it. With cancelable = true the back key closes it (a tap outside does not); closeCallback fires in both cases. |
| iOS | A spinner box above the app, also above native video views. It does not block touches: taps reach your app below. Alerts and dialogs opened while a spinner is visible wait until it is gone (at most 60 seconds), so the user sees one at a time. cancelable is not used. |
| macOS | A sheet on the app window with the app icon, spinner, title and message. cancelable is not used; your code closes it. Show it only when the app window is visible. |
private const PROGRESS_ID:String = "connect"; public function connect():void { dialog.progressShow( PROGRESS_ID, "Connecting", "Contacting the server…", true, function():void { // once: closed by progressDismiss() or, on Android, by the back key if ( !connected ) cancelConnect(); // your app } ); startConnect(); // your app; calls onStep() and onConnected() / onFailed() } public function onStep( text:String ):void { dialog.progressUpdate( PROGRESS_ID, text ); // e.g. "Signing in…" } public function onConnected():void { connected = true; dialog.progressDismiss( PROGRESS_ID ); } public function onFailed( reason:String ):void { dialog.progressDismiss( PROGRESS_ID ); dialog.show( "Connection failed", reason ); // on iOS shown after the spinner is gone }
Light and dark theme
The last parameter of present() and progressShow() is theme: "light" or "dark".
| Android | "dark" uses the device's dark dialog theme; anything else, including the default "", uses the light theme. |
| iOS | "dark" or "light" force that appearance; "" follows the system setting. |
| macOS | Not used: dialogs always follow the system appearance. |
show() has no theme parameter: it uses the light theme on Android and follows the system on iOS and macOS.
var theme:String = appUsesDarkMode() ? "dark" : "light"; // your app's setting dialog.present( "Night mode", "Switch the camera to night mode?", true, [ { label:"No", style:2, tag:0 }, { label:"Yes", style:1, tag:1 } ], null, function( tag:int, values:Array ):void { if ( tag == 1 ) enableNightMode(); }, null, // no id needed theme ); dialog.progressShow( "wait", "", "Please wait…", false, null, theme );
App lifecycle and screen changes
Dialogs are native windows of your app. They stay open while the app is in the background, and the callback fires when the user returns and chooses a button. Because every call is asynchronous, keep the state your callback needs (for example in member variables) instead of relying on what was on screen when the dialog opened. If the situation changes while a dialog is open, close it with presentDismiss() or progressDismiss().
On Android the dialog keeps the app's immersive full-screen mode; dialogs without fields are centred, dialogs with fields sit at the top and adapt to the space above the keyboard when it opens or the device rotates. On iOS, alerts are centred on the screen, also on devices whose safe area is uneven, such as an iPhone in landscape.
Diagnostics on iOS
log(msg) writes a line prefixed with "KlugDialog:" to the iOS device log, which you can read in the Console app on a Mac while the device is connected. It is meant for diagnostics in development builds and does nothing on Android and macOS.
dialog.log( "login screen shown, user=" + userName ); // appears as "KlugDialog: login screen shown, …"
API reference
com.klug.dialog.Dialog
Extends flash.events.EventDispatcher. Singleton. All dialog methods are asynchronous and return at once.
| getInstance() | public static function getInstance():Dialog Returns the single instance and creates it on the first call. Calling new Dialog() a second time throws an Error. |
| isSupported | public function get isSupported():Boolean True on Android, iOS devices and macOS; false elsewhere (Windows, iOS Simulator, other platforms). |
| show() | public function show(title:String, message:String, buttons:Array = null, callback:Function = null, style:int = 0):Boolean Alert with up to three buttons. buttons: array of labels, default ["OK"]. callback(index:int): position of the chosen button, or -1 if closed without a button (Android back key or tap outside; iOS if the alert could not be presented). style: one of the STYLE constants, used on macOS. Returns true if the dialog was handed to the native side, false if unsupported. |
| present() | public function present(title:String, message:String, cancelable:Boolean, buttons:Array, fields:Array, callback:Function, reqIdOverride:String = null, theme:String = ""):Boolean Dialog with any number of buttons and text fields. buttons: array of {label:String, style:int, tag:int}; null or empty gives one "OK" button with tag 0. fields: array of {placeholder:String, secure:Boolean, text:String, keyboardType:int, autocorrect:Boolean, autocap:int}, or null. callback(tag:int, values:Array) or callback(tag:int, values:Array, cancelled:Boolean = false). cancelable: back key and tap outside close the dialog (Android). reqIdOverride: your id for presentDismiss(). theme: "light" or "dark" (Android, iOS). Returns true if handed to the native side, false if unsupported. |
| presentDismiss() | public function presentDismiss(reqId:String):void Closes the present() dialog shown with this id. Its callback is not called. No effect if no such dialog is open. |
| progressShow() | public function progressShow(reqId:String, title:String, message:String, cancelable:Boolean, closeCallback:Function = null, theme:String = ""):Boolean Progress dialog with a spinner and no buttons. closeCallback() is called once when it has closed. cancelable: back key closes it (Android). theme: "light" or "dark" (Android, iOS). Returns true if handed to the native side, false if unsupported. |
| progressUpdate() | public function progressUpdate(reqId:String, message:String):void Changes the message of the open progress dialog with this id. An empty message hides the line (Android, iOS). |
| progressDismiss() | public function progressDismiss(reqId:String):void Closes the progress dialog with this id; its closeCallback follows. |
| log() | public function log(msg:String):void Writes msg to the iOS device log. No effect on Android and macOS. |
| STYLE_INFO | public static const STYLE_INFO:int = 0 Informational sheet (macOS). |
| STYLE_WARNING | public static const STYLE_WARNING:int = 1 Warning sheet (macOS). |
| STYLE_CRITICAL | public static const STYLE_CRITICAL:int = 2 Critical sheet (macOS). |
Button object (present)
| label:String | Button text. Default "OK". |
| style:int | 0 default, 1 positive, 2 negative, 3 destructive, 4 cancel, 5 neutral. See the guide for the effect per platform. |
| tag:int | Value passed to the callback when this button is chosen. |
Field object (present)
| placeholder:String | Hint text. All platforms. |
| secure:Boolean | Hidden input. All platforms. |
| text:String | Initial text. All platforms. |
| keyboardType:int | 0–10, see the keyboard table. Android and iOS. |
| autocorrect:Boolean | Default false. Android and iOS. |
| autocap:int | 0 none (default), 1 sentences, 2 words, 3 all. Android and iOS. |
Events
KlugDialog does not dispatch public events. All results arrive through the callbacks you pass, asynchronously and on the ActionScript thread:
| show() callback | callback(index:int): position of the chosen button (0 = first), or -1 if closed without a button. |
| present() callback | callback(tag:int, values:Array): tag of the chosen button and the field texts in field order (an empty array without fields). |
| present() cancelled | callback(-1, [], true): closed without a button. Only for callbacks that declare a third parameter. |
| progressShow() closeCallback | closeCallback(): once, when the progress dialog has closed (by code, or by the back key on Android). |
Each callback fires at most once. After presentDismiss() the callback of that dialog does not fire.
Troubleshooting
isSupported is false on iOS
The native part is built for iOS devices. In the iOS Simulator the fallback is used, so isSupported is false there. Test dialogs on a device, and keep a fallback for platforms without support.
My present() callback is not called when the user presses the back key
Cancellation is reported only to callbacks that declare a third parameter: function(tag:int, values:Array, cancelled:Boolean = false):void. With two parameters the callback is skipped for a cancel.
The button order differs between platforms
Each platform arranges buttons by its own rules. Use the tag in present() to identify the chosen button rather than its position. In show() the index always refers to your array, whatever the visual order.
The style parameter of show() has no effect on Android or iOS
That is intended: the alert styles exist on macOS. For a red destructive button on Android and iOS use present() with button style 3.
On iOS my alert appears only after a delay
A dialog waits while a progress spinner is visible, so the user does not see both at once. Close the spinner with progressDismiss() before you show the next dialog, or expect the dialog to appear when the spinner is gone (at most 60 seconds later).
Taps go through the progress spinner on iOS
By design: on iOS the spinner does not block your app. If the user must not interact during the operation, disable your own controls while it runs.
The progress sheet does not appear on macOS at startup
A sheet needs a visible app window. Show the progress dialog after your main window is visible.
The closeCallback of my progress dialog fires too early
Calling progressShow() a second time with an id that is still open can end the first dialog's life cycle and call the callback, notably on Android. Keep one progressShow() per id and change the text with progressUpdate(); after progressDismiss(), wait for the closeCallback before you reuse the id, or use a new id.
The values array contains empty strings
An empty field returns ""; the array always has one entry per field. Validate the input in your callback and show the dialog again if needed, for example with the entered text as the field's text.
The first letter of user names or e-mail addresses is capitalised
Auto-capitalisation is off unless you set autocap. If you see capitals, check that the field object does not set autocap or autocorrect, and use keyboardType:7 for e-mail addresses.
Changelog
1.2.1
- iOS: alerts are centred on the screen also when the left and right safe areas differ
- Android: the keyboard closes when a dialog with text fields closes, by button, cancel or presentDismiss()
1.2.0
- iOS support: alerts, text fields and button lists as native UIAlertController, progress spinner above native video views
- iOS: dialogs wait while a progress spinner is visible; log() for diagnostics
- Android support: native dialogs with fields that stay above the keyboard, progress dialog, stacked button lists, dismiss from code, cancel callback for the back key and taps outside
- present() and progressShow() take a light or dark theme; fields take keyboard type, auto-correction and auto-capitalisation
1.1.x
- macOS: alert sheets show the app icon
- macOS: progress sheet (progressShow, progressUpdate, progressDismiss) and closing present() dialogs from code with your own id
- First release for macOS: native alert sheets with up to three buttons and three styles, present() with tagged buttons and text fields