Guides
Navigation
Stacks, tabs, sheets, alerts and menus, windows and the menu bar, links into the app, and shares from other apps.
An app's structure is made of stacks of screens, tabs, sheets, alerts and menus, and on a desktop, windows and the menu bar. Each one is the platform's own:
- iOS: a
UINavigationController, aUITabBarController, sheet presentations,UIAlertController,UIMenu. - Android: a top app bar with Material transitions and predictive
back, Material's bottom navigation bar, a
BottomSheetDialog, Material dialogs,PopupMenu. - macOS: the window's toolbar (the back button, the screen's bar
buttons and search field) and title, an
NSTabView, sheet windows,NSAlert,NSMenu, and the menu bar. - Windows: a header bar over the screen, a tab bar as WinUI's
NavigationViewat the top, modal windows, task dialogs, popup menus, and the window's menu bar, drawn as Windows 11's. - Linux: libadwaita's: the window's header bar,
AdwNavigationView, a view switcher,AdwDialog,AdwAlertDialog, GTK's popover menus, and the main menu. - The web: a header bar of the host's,
<dialog>s and popovers, and the browser's back button (see the web).
This page covers them, along with going back, and the links and shares
that open the app. examples/navigation has
all of them; examples/notes has a menu bar and a
window.
Stacks
ui.Stack is a navigation stack. It starts with a root screen, and
components in it push more with the navigator from ui.UseNavigator.
func App() ui.Node {
return ui.Stack(Notes, ui.ScreenOptions{Title: ui.Val("Notes")})
}
func Notes() ui.Node {
nav := ui.UseNavigator()
return ui.List(ui.ListProps{}, notes, func(n note) int { return n.ID },
func(n signal.Accessor[note], _ signal.Accessor[int]) ui.Node {
return ui.View(ui.ViewProps{
OnPress: func() {
v := n.Get() // in a handler: not tracked
nav.Push(func() ui.Node { return Detail(v) }, ui.ScreenOptions{Title: ui.Val(v.Text)})
},
Style: ui.Style{Padding: ui.Symmetric(12, 20)},
}, ui.Text(signal.Func(func() string { return n.Get().Text })))
})
}
func Detail(n note) ui.Node {
nav := ui.UseNavigator()
return ui.SafeArea(ui.Style{Padding: ui.All(20), Gap: 12},
ui.StaticText(n.Text),
ui.Button(ui.ButtonProps{Title: ui.Val("Done"), OnPress: nav.Pop}),
)
}nav.Push(screen, options)shows a screen on top, with the platform's push animation.nav.Pop()removes the top screen. It never pops the root.nav.Depth()is how many screens the stack holds.
Call ui.UseNavigator while the component is being built, as above, and
keep the navigator for its handlers. It panics outside a stack.
On iOS and Android each stack has its own bar, and so it does on
Windows (a header bar the host draws over the screen, as WinUI's) and
on the web. On a Mac and on Linux a window has one bar, as their apps
do, and it belongs to the innermost stack on screen (in tabs, the shown
tab's): its back button, its top screen's title, bar buttons and search
field. On a Mac that's the window's toolbar and title; on Linux the
window's AdwHeaderBar. Pushes and pops slide on macOS, Linux and the
web; on Windows, screens change without a transition.
A stack fills the space it's given. Each screen is an owner: popping it, by code or by the user's back gesture, disposes it, stopping its effects. A screen pushed again starts fresh, so keep what must outlive it in a signal outside it, or in storage.
Screen options
ui.ScreenOptions configures a screen of a stack, given to Stack for
the root and to Push for the others.
Titleis the navigation bar's title (the top app bar's on Android, the header bar's on Windows, Linux and the web; on a Mac, the window's title; in a browser, the page's title too). It's an accessor, so it can change.Actionsare the bar's buttons.NoBackGestureturns the back gesture off for the screen (see Back).Searchputs a search field in the bar.ZoomFromzooms the screen out of a view on the screen below.LargeTitleshows the title large at the top of the screen on iOS, collapsing into the bar as it scrolls. Android's top app bar, the desktops' bars and the web's stay as they are. The bar follows the first scroll view that takes up most of the screen, so something can be behind it.BarColoris the top app bar's color on Android, and the header's on the web, to continue a background the screen draws itself. iOS's bar is clear at the top of a screen already; the desktops' bars keep their own.
Bar buttons
Each action is a ui.Action. With an Icon, the bar shows the icon
instead of the title, and screen readers read the title (on a desktop,
it's the icon's tooltip too).
nav.Push(func() ui.Node { return Detail(n) }, ui.ScreenOptions{
Title: ui.Val(n.Text),
Actions: []ui.Action{
{Title: ui.Val("Share"), Icon: ui.IconShare, OnPress: func() { ui.Share(n.Text) }},
{Title: ui.Val("Delete"), Role: ui.ActionDestructive, OnPress: func() { nav.Pop() }},
},
})On Android, actions go in the top app bar as there's room, and the rest go in its overflow menu. A long press on an icon shows its title. On a Mac they're toolbar items, on Windows buttons at the header bar's end, and on Linux buttons in the header bar.
Search
Search puts a search field in the bar, as SwiftUI's searchable does.
Typing writes Text, and writing Text from code changes the field.
Placeholder is "Search" when it's empty, and OnSubmit runs on the
keyboard's search key, or Enter on a desktop.
query := signal.New("")
shown := signal.Func(func() []note {
var out []note
for _, n := range notes.Get() {
if strings.Contains(strings.ToLower(n.Text), strings.ToLower(query.Get())) {
out = append(out, n)
}
}
return out
})
return ui.Stack(func() ui.Node {
return ui.List(ui.ListProps{}, shown, func(n note) int { return n.ID },
func(n signal.Accessor[note], _ signal.Accessor[int]) ui.Node {
return ui.Text(signal.Func(func() string { return n.Get().Text }), ui.Style{Padding: ui.Symmetric(12, 20)})
})
}, ui.ScreenOptions{Title: ui.Val("Notes"), Search: &ui.Search{Text: query, Placeholder: ui.Val("Search notes")}})On iOS it's a UISearchController under the title, which stays shown
as the screen scrolls. On Android it's Material's pattern: a search
button in the top app bar that expands into the field. Button: true keeps
it out of the way on iOS too, for a screen with its own controls at the bottom:
a button in the bar on iOS 26, and before that, hidden above the content until
it's pulled down.
On a desktop and on the web, the field is in the screen's bar:
- macOS: a search field in the toolbar (
NSSearchToolbarItem). - Windows: a Fluent text box with the search glyph, before the bar buttons, as WinUI puts a search box in a command bar.
- Linux: a search button in the header bar, and GTK's search bar under it, as GNOME's apps search; typing anywhere in the window opens it.
- The web: a field under the header (a button in it, with
Button: true).
Zooming from a view
ZoomFrom is the Style.Match key of a view on the screen below, such
as a photo in a grid or a card. The screen zooms out of that view as
it's pushed, and back into it as it's popped. See
shared elements.
key := fmt.Sprintf("photo-%d", id)
return ui.View(ui.ViewProps{
Label: ui.Val("Photo"),
Style: ui.Style{AspectRatio: 1, Match: key},
OnPress: func() {
nav.Push(func() ui.Node { return photo(id) }, ui.ScreenOptions{Title: ui.Val("Photo"), ZoomFrom: key})
},
})On iOS this is the system's zoom transition (iOS 18 and later; before
that, the screen pushes as usual). Android does its own zoom, and
popping with the predictive back gesture goes back into the view. The
desktops and the web push and pop as usual.
examples/gallery's photo grid does this.
Back
Each platform goes back its own way, and Asuka leaves it so:
- iOS: the bar's back button, and a swipe from the screen's leading
edge.
NoBackGestureturns the swipe off. The back button still pops. - Android: the system back, by the button or the gesture, and the
up arrow in the top app bar. Back pops the innermost stack on screen
that has a screen to go back to: in tabs, the shown tab's stack. With
the predictive back gesture, the top screen shrinks and follows the
finger, showing the one below. When no stack has a screen to pop,
back does what it does in any app, and leaves it.
NoBackGestureturns off the predictive animation, but back still pops the screen. - macOS: the toolbar's back button, and View ▸ Back (⌘[), which is off while there's no screen to go back to. Escape dismisses a sheet.
- Windows: the header bar's back button, and Alt+Left, which pops the innermost stack of the window it's pressed in. Escape dismisses a sheet.
- Linux: libadwaita's: the header bar's back button, Alt+Left,
Escape, and a swipe on a touchpad or touch screen.
NoBackGestureturns the swipe off; the back button still pops. - The web: the header's back button, the browser's back button (and a phone's back gesture), Escape, Alt+Left (⌘[ on a Mac), and a swipe from the leading edge on a touch screen. See the web.
NoBackGesture changes nothing on macOS and Windows, which have no
back gesture. Whichever way the user goes back, the screen is disposed,
as with nav.Pop(). Back closes an open sheet or dialog first.
Taking back yourself
ui.OnBack lets a component decide, when the user goes back, whether
to go: to ask before throwing away a draft, or to close something of
the screen's own first (a search, an expanded panel). The handler
returns true when it took the back; when every handler returns false,
going back goes on.
func editor() ui.Node {
dirty := signal.New(false)
nav := ui.UseNavigator()
ui.OnBack(func() bool {
if !dirty.Peek() {
return false // nothing to lose: the screen pops
}
ui.Alert(ui.AlertProps{
Title: "Discard changes?",
Actions: []ui.Action{
{Title: ui.Val("Keep editing"), Role: ui.ActionCancel},
{Title: ui.Val("Discard"), Role: ui.ActionDestructive, OnPress: func() {
dirty.Set(false)
nav.Pop()
}},
},
})
return true
})
// ...
}It belongs to where the component is built, and goes with it:
- A pushed screen: its back button, Android's back, the edge swipe
on iOS, ⌘[ on a Mac, Alt+Left on Windows and Linux, the browser's
back. While the screen has a handler, iOS's and Linux's swipe is off
on it (the swipe can't be asked, only stopped, as with UIKit's own
apps) and Android doesn't preview the pop. iOS's back button is the
screen's
backAction. - A sheet: a swipe down, a tap outside, Android's back, Escape on a
desktop or in a browser, a sheet window's close button on Windows.
While it has a handler, the sheet can't be swiped away: trying asks
the handler (iOS's
isModalInPresentation; a sheet that isn't cancelable on Android). When the handler lets it go, the sheet closes andOnDismissruns, as if the user had dismissed it. - Elsewhere (outside any screen or sheet, or on a stack's first screen, which back leaves): Android's back with no screen to pop, where the app would otherwise close, and the browser's back, where the page would go back. iOS and the desktops have no such back.
The latest handler is asked first. nav.Pop() and closing a sheet from
code don't ask: only the user's going back does.
Split views
ui.SplitView is an app's top level when it has many destinations:
a sidebar of them, and the chosen one's screens beside it (SwiftUI's
NavigationSplitView).
ui.SplitView(ui.SplitViewProps{Title: ui.Val("Mail"), Sections: []ui.SidebarSection{
{Destinations: []ui.Destination{
{Title: ui.Val("Inbox"), Icon: ui.IconMail, Content: inbox},
{Title: ui.Val("Sent"), Icon: "paperplane", Content: sent},
}},
{Title: ui.Val("Folders"), Destinations: folders},
}})Each destination's Content is the first screen of a stack: it pushes
with ui.UseNavigator, and choosing another destination replaces the
stack's screens with its first, at once, disposing them. Selected is
the chosen destination, counted across the sections; the user's choice
writes it, and writing it shows another.
| Compact (a phone) | Wide | |
|---|---|---|
| iOS | the split view collapsed into a stack, as iOS does it: the sidebar is the first screen, an inset grouped list; a row pushes its destination, and back returns to the list | UISplitViewController's sidebar beside the content (Liquid Glass on iOS 26), which the user can hide |
| Android | Material's modal navigation drawer, opened by the menu button of the destination's first screen (or a swipe from the edge), closed by choosing or by back | a standard drawer, standing open beside the content (840dp and wider) |
| macOS | NSSplitViewController, its sidebar a source list the user can collapse |
|
| Windows | WinUI's NavigationView pane: the destinations beside the content, the chosen one with an accent pill |
|
| Linux | under 500 points wide, libadwaita's collapsed split view: the sidebar, then the chosen destination's screens, with a back button to the sidebar | AdwNavigationSplitView: a sidebar page of its own, its title in its own header bar, beside the content under the window's |
| The web | a drawer, opened from the detail's header | a sidebar beside the detail |
Tabs
ui.Tabs shows one page at a time, with a tab bar:
- iOS: a
UITabBarController. - Android: Material's bottom navigation bar.
- macOS: an
NSTabView, its tabs on top. - Windows: a bar of tabs on top, as WinUI's top
NavigationView: an icon and a title each, an accent pill under the selected one; the arrow keys move between them. - Linux: an
AdwViewStack, its view switcher in the header bar on a wide window and a bar at the bottom of a narrow one (under 550 points), as GNOME's apps have it. - The web: a tab bar at the bottom.
Each ui.Tab has a Title, an Icon and its Content.
Selected is the shown tab's index. Choosing a tab writes it, and
writing it switches tabs.
tab := signal.New(0)
return ui.Tabs(ui.TabsProps{Selected: tab},
ui.Tab{Title: ui.Val("Notes"), Icon: ui.IconList, Content: func() ui.Node {
return ui.Stack(Notes, ui.ScreenOptions{Title: ui.Val("Notes")})
}},
ui.Tab{Title: ui.Val("Settings"), Icon: ui.IconSettings, Content: func() ui.Node {
return ui.Stack(Settings, ui.ScreenOptions{Title: ui.Val("Settings")})
}},
)Give each tab its own Stack so that it has a bar and keeps its own
history, as the platforms' apps do. Every tab's content is built when
the Tabs is, and kept while you switch between them. Tab icons are
symbols, like a button's: see Symbols and icons.
On Android an icon must be one that Android has.
Sheets
ui.Sheet presents content in a modal sheet while Open is true:
- iOS: a sheet presentation with detents.
- Android: a
BottomSheetDialog. - macOS: a sheet window, attached to the window in use.
- Windows: a modal window over the window in use, which is disabled meanwhile.
- Linux: an
AdwDialog: a floating dialog, or a bottom sheet in a narrow window. - The web: a modal
<dialog>, a bottom sheet on a narrow window and a centered card on a wide one.
The node it returns takes no space, so put it anywhere in the screen that opens it.
open := signal.New(false)
note := signal.New("")
return ui.Column(nil,
ui.Button(ui.ButtonProps{Title: ui.Val("Add a note"), OnPress: func() { open.Set(true) }}),
ui.Sheet(ui.SheetProps{
Open: open,
Detents: ui.DetentMedium | ui.DetentLarge,
Grabber: true,
Content: func() ui.Node {
draft := signal.New(note.Peek())
return ui.SafeArea(ui.Style{Padding: ui.All(20), Gap: 12},
ui.TextField(ui.TextFieldProps{Value: draft, Placeholder: ui.Val("Write a note")}),
ui.Button(ui.ButtonProps{Title: ui.Val("Save"), Variant: ui.ButtonFilled, OnPress: func() {
note.Set(draft.Peek())
open.Set(false)
}}),
)
},
OnDismiss: func() { fmt.Println("dismissed, not saved") },
}),
)Detentsare the heights it can rest at:ui.DetentMedium(half the screen),ui.DetentLarge(all of it), or both, which starts at medium and lets the user drag it up. With none, it's large. When the keyboard shows, a sheet that can be large goes large. On a desktop a medium-only sheet is half the window's height, and any other most of it; the user resizes a sheet window on a Mac.Grabbershows the handle at its top (iOS, Android, and the web's bottom sheet).Contentis built when it opens and disposed when it closes, so state made in it starts fresh each time.
The user dismisses a sheet by swiping it down, on Android with back
or a tap outside it, and on a desktop or in a browser with Escape (on
Windows, its window's close button too; on the web, a click outside).
That sets Open to false and calls OnDismiss.
Setting Open to false from code closes it without calling
OnDismiss. So OnDismiss means "closed without your say", such as a
draft left unsaved.
Popovers
ui.Popover shows content in a bubble pointing at a view, over the
screen, while Open is true: a few options, a detail, a small form.
Its children are what it points at:
open := signal.New(false)
return ui.Popover(ui.PopoverProps{Open: open, MaxWidth: 280, Content: func() ui.Node {
return ui.Column(ui.Style{Padding: ui.All(16), Gap: 8},
ui.StaticText("Shared with 3 people", ui.Style{Font: ui.Headline}),
ui.Button(ui.ButtonProps{Title: ui.Val("Manage"), OnPress: manage}))
}}, ui.Button(ui.ButtonProps{Icon: "person.2", Label: ui.Val("Sharing"), OnPress: func() { open.Set(true) }}))It's as big as its content, no wider than MaxWidth (320 by default),
text wrapping to it, and no taller than most of the screen. A tap
outside dismisses it, setting Open false and calling OnDismiss, as a
sheet's; ui.OnBack in it can keep it.
| The view | Where | |
|---|---|---|
| iOS | UIPopoverPresentationController, a popover on iPhone too (not adapted into a sheet), Liquid Glass on iOS 26 |
where UIKit fits it, its arrow on the anchor |
| Android | a PopupWindow on Material's container surface, 16dp corners, elevated |
below the anchor, centered (above it without room) |
| macOS | NSPopover, transient |
below the anchor, where AppKit fits it |
| Windows | a popup window, light dismissed, Windows 11's corners (WinUI's Flyout) | below the anchor, centered (above it without room) |
| Linux | a GtkPopover; Escape closes it too |
below the anchor, where GTK finds room |
| The web | a popover | next to the anchor |
Alerts and action sheets
ui.Alert shows an alert now:
- iOS: a
UIAlertController. - Android: a Material dialog.
- macOS: an
NSAlert, shown as a sheet over the window in use. - Windows: a task dialog over the window in use.
- Linux: an
AdwAlertDialog. - The web: a modal
<dialog>.
Pressing a button closes the alert and runs that button's OnPress.
With no Actions, it has a single OK.
ui.Alert(ui.AlertProps{
Title: "Delete this note?",
Message: "You can't undo this.",
Actions: []ui.Action{
{Title: ui.Val("Cancel"), Role: ui.ActionCancel},
{Title: ui.Val("Delete"), Role: ui.ActionDestructive, OnPress: remove},
},
})An action's Role says what it is:
ui.ActionDefaultis an ordinary button.ui.ActionCancelis the way out. iOS puts it in its own place, and Android makes it the dialog's negative button. On a desktop, Escape presses it: on a Mac it's the last button, after the default one.ui.ActionDestructiveis red: in iOS's style, in Material's error color on Android, AppKit's destructive button on a Mac, and libadwaita's destructive response on Linux.
On Android, the last other button is the dialog's positive button, at
the end, and a second one is neutral. With more than two buttons other
than Cancel, the dialog shows them as a list. On a Mac the first button
other than Cancel is the default, which Return presses; on Linux the
first ui.ActionDefault one is, in the accent color.
ActionSheet shows the actions as an action sheet, a list of choices
coming up from the bottom on iOS:
ui.Alert(ui.AlertProps{
Title: "Photo",
ActionSheet: true,
Actions: []ui.Action{
{Title: ui.Val("Take a photo"), OnPress: func() {}},
{Title: ui.Val("Choose from library"), OnPress: func() {}},
{Title: ui.Val("Cancel"), Role: ui.ActionCancel},
},
})On Android an action sheet is a dialog with the actions as a list, and
its Message isn't shown. On Windows its buttons are a task dialog's
command links; on Linux it's an alert dialog with no default button; on
a Mac it's an alert as any other; on the web, a dialog whose buttons are
a list. An alert closed some other way, such as back or a tap outside it
on Android, runs no button's OnPress.
Menus
A menu shows a list of ui.Actions, the same type as alert buttons and
bar buttons, with an optional Icon beside each title. There are two:
- A
ui.ButtonwithMenuopens it when tapped. ui.Menuputs one on any content: on a tap, or withLongPressas a context menu.
On iOS it's a UIMenu, and on Android a PopupMenu. On a Mac it's an
NSMenu, on Windows a popup menu (a destructive item red, as on a Mac),
on Linux a GtkPopoverMenu, and on the web a popover. On the desktops
and the web a context menu opens with a right-click (a secondary
click), and on Windows, Linux and the web with a long press too. Both are in
Components. Rows of a List have swipe actions
too: see Lists.
Windows
A desktop app has a main window, which holds what app.Run shows. To
give it a title, a size to open at and a least size, start the app with
app.RunWindow:
func main() {
app.RunWindow(ui.WindowOptions{Width: 420, Height: 640, MinWidth: 320, MinHeight: 360}, App)
}Sizes are in points, of the window's content. With none, the main window
opens at 800 by 600 points, and the user can't make it smaller than 320
by 320. Title is the app's name when it's empty; it's an accessor, so
it can follow the app's language. The window comes back at the size the
user left it, which wins over Width and Height: on a Mac its frame,
on Windows where it was and whether it was maximized, on Linux its size
and whether it was maximized (a Wayland app can't place its window).
iOS and Android ignore the options.
ui.Window opens another window while Open is true, as ui.Sheet
opens a sheet:
stats := signal.New(false)
return ui.Column(nil,
ui.Button(ui.ButtonProps{Title: ui.Val("Statistics"), OnPress: func() { stats.Set(true) }}),
ui.Window(ui.WindowProps{
Open: stats,
Title: ui.Val("Statistics"),
Width: 280, Height: 140,
Content: func() ui.Node {
return ui.Text(signal.Func(func() string { return fmt.Sprintf("%d notes", len(notes.Get())) }))
},
}),
)Contentis built when the window opens and disposed when it closes.ui.UseEnvironmentin it is the window's: its size follows the user resizing it, and its scale is its display's.- The user closing it sets
Opento false and callsOnClose. SettingOpento false from code closes it without callingOnClose. - It opens at 800 by 600 points unless it says otherwise, cascaded from the window in use on a Mac and on Windows. On Linux the desktop places it, as GTK 4 leaves to it (GNOME and KDE put a new window where it covers the fewest others). Stacks in it have their own bar, as in the main window. Sheets and alerts open over the window the user is working in.
| A window | |
|---|---|
| macOS | an NSWindow, with its own toolbar and title (its innermost stack's) |
| Windows | a top-level window, with its own title and taskbar button |
| Linux | an AdwWindow, with its own header bar |
| The web | a panel in the page, which the user can move and resize |
A phone has no windows: opening one on iOS or Android panics. A
component that offers one checks ui.UseEnvironment().Platform first,
or opens it only from what only a desktop has, as the menu bar below.
The menu bar and keyboard shortcuts
ui.MenuBar gives a desktop app its menus, and their items their
keyboard shortcuts:
ui.MenuBar(signal.Static([]ui.MenuBarMenu{
{Title: "File", Items: []ui.MenuItem{
{Title: "New Note", Shortcut: ui.Cmd("n"), OnPress: add},
ui.MenuSeparator,
{Title: "Export…", Shortcut: ui.Cmd("e").With(ui.Shift), OnPress: export},
}},
{Title: "Edit", Items: []ui.MenuItem{
{Title: "Clear Completed", Shortcut: ui.Cmd("k").With(ui.Shift), Destructive: true, OnPress: clear},
}},
}))- A
ui.MenuItemhas aTitle, aShortcutand itsOnPress;Disabled,CheckedandDestructive(in red) change how it shows.ui.MenuSeparatoris a line between groups of items. - A
ui.Shortcutis aKeyand itsMods.ui.Cmd(key)is the key with the platform's command key: ⌘ on a Mac, Ctrl elsewhere..With(ui.Shift),ui.Alt(a Mac's Option) andui.Controladd modifiers. A key is a character ("n",",","=") or a name:enter,escape,tab,space,delete,backspace,up,down,left,right,home,end,pageup,pagedown,f1tof12. - The menus follow their accessor: an item turned off or checked, a menu
that comes and goes. While several
MenuBars are mounted, the last one shows; when it's disposed, the one before does. The node it returns takes no space; put it anywhere.
Each desktop shows them its own way:
- macOS: the menu bar, beside the Mac's own menus: the app's menu (About, Hide, Quit), File, Edit (Undo, Cut, Copy, Paste...), View (Back, Enter Full Screen) and Window. A menu titled File, Edit, View, Window or Help adds its items to that one; the others go between View and Window.
- Windows: a menu bar on the main window, its shortcuts working in every window of the app. Each menu and item gets an access key (Alt+F), and each item its shortcut after its title ("Ctrl+Shift+K").
- Linux: GNOME's apps have no menu bar, but a main menu: the header bar's last button. Each of the app's menus is a submenu of it (one menu's items are the main menu's own), and the shortcuts work in every window.
- The web: the shortcuts work in the page; it shows no menu bar.
iOS and Android ignore MenuBar, so an app can have one wherever it
runs. Its items are commands, so keep each reachable from the
interface too, for a phone.
Toasts
Asuka has no toast component, since iOS and Android have no standard one
that apps share. Make one from a view placed over the screen,
shown with ui.Show and brought in with a
transition. The gallery's toast does this, and
calls ui.Announce so that screen readers say it too. See
examples/gallery.
Deep links
An app opens for URLs of its own scheme, and for https links to its own
domains, when asuka.json lists them:
{
"name": "Notes",
"id": "com.example.notes",
"scheme": "notes",
"links": ["notes.example.com"]
}schemeopens the app fornotes://anything: on iOS and Android, on a Mac (the app'sInfo.plistdeclares it), and on Linux once the app is installed (its desktop file handles the scheme; a link opened while the app runs goes to it, as GNOME's apps are one instance each), and on Windows (the app registers it for the user as it starts, underHKEY_CURRENT_USER\Software\Classes, again if it has moved; a link opened while the app runs goes to it). An app with anidruns once on Linux and Windows: launched again, it comes to the front.asuka devregisters no scheme on Linux or Windows, and its runs don't hand themselves to an installed copy of the app: try links with a build.linksopens it forhttps://notes.example.com/...: universal links on iOS, App Links on Android. The domain must say so too. It servesapple-app-site-associationfor iOS andassetlinks.jsonfor Android, as each platform's docs describe. The desktops don't open for them.
ui.OnOpenURL gets each URL the app is opened with, at launch or while
it runs. The URL that launched the app comes before any handler exists,
so it goes to the first handler. The handler stops with its component's
owner. Route from it with the navigator, or by setting a signal.
ui.OnOpenURL(func(raw string) {
u, err := url.Parse(raw)
if err != nil {
return
}
if u.Host == "add" {
fmt.Println("add", u.Query().Get("text"))
}
})examples/notes adds a note for
asukanotes://add?text=.... A notification's URL arrives the same way
when the user taps it, on every platform (see Device). A
web app has no links of its own: its page's address doesn't change as it
navigates, and opening it doesn't call OnOpenURL.
Shares from other apps
With "shareTarget": true in asuka.json, the app shows in other apps'
share sheets, for text and links, such as "Share" in a browser or in
YouTube; {"types": ["image/*", ...]} takes files too (see
Device). ui.OnShare gets
what's shared. A share that launched the app goes to the first handler,
as with OnOpenURL.
ui.OnShare(func(items []ui.SharedItem) {
for _, it := range items {
fmt.Println("shared:", it.Kind, it.Text, it.Path)
}
})On iOS the app comes with a share extension, which opens the app with
what's shared, as Android opens it: the extension shows no screen of
its own. What's shared is often a title and a link together (a page's
title, a text, and its URL, a link, from Safari).
examples/downloader looks for the link. The
desktops and the web aren't share targets: OnShare is never called
there.
To show a sheet of your app's over the app that shares, without opening
yours, see app.ShareSheet in
Device. To share
from your app instead, ui.Share(text) shows the system's share sheet,
and ui.ShareFiles shares files: the share menu on a Mac, the share UI
on Windows, the browser's on the web. Linux has no share sheet: they
copy what's shared to the clipboard, and say so in a toast.