Back to Wails

Your First Mobile App

docs/src/content/docs/guides/mobile/first-mobile-app.mdx

2.15.015.0 KB
Original Source

import { Steps, Tabs, TabItem, Aside, Card, CardGrid } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; import iosSimApp from '../../../../assets/ios-simulator-first-app.png';

This guide takes a standard Wails desktop app and runs it on iOS Simulator or Android Emulator. You do not need to change your Go code. The same main.go builds for all targets.

Time to complete: 15–30 minutes (most of that is toolchain installation on first run)

Start from a desktop project

If you don't have one yet, create a fresh project:

bash
wails3 init -n mymobileapp
cd mymobileapp

Confirm the desktop app works first:

bash
wails3 dev

Once it opens, quit and move on. Everything that runs on desktop also runs on mobile — you won't need to touch main.go or any Go code for this guide.


Choose your platform

<Tabs syncKey="mobile-platform"> <TabItem label="iOS Simulator" icon="apple">

Requirements

  • macOS (iOS builds are macOS-only)

  • Full Xcode — not just command-line tools. Install from the App Store, then:

    bash
    sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
    sudo xcodebuild -license accept
    
  • Go 1.25+ and npm (already installed if you ran wails3 init)

Run wails3 doctor to verify — it lists the iOS SDKs it can find.

Run on the Simulator

<Steps>
  1. Launch the app

    bash
    wails3 task ios:run
    

    That's it — this builds your app, boots a simulator if one isn't already running, and launches it.

    <Aside type="tip"> The first run takes a few minutes (it's compiling and caching the Wails framework for iOS). Every run after that is much faster. </Aside>

    When it launches, your unmodified desktop app is running on the iOS Simulator — same main.go, same frontend:

    <Image src={iosSimApp} alt="A default Wails app running on the iOS Simulator" width={300} />
  2. Stream logs

    In a separate terminal:

    bash
    wails3 task ios:logs:dev
    

    This tails the simulator log, filtered to your app. fmt.Println and log.Println output appears here.

  3. Inspect the WebView

    In Safari: Develop → Simulator → your app. The full Web Inspector works — console, debugger, network panel, everything.

  4. Make a change

    Edit any frontend file (frontend/src/main.js, index.html, etc.) and re-run wails3 task ios:run. Wails rebuilds the frontend and relaunches the app.

    For Go changes: also re-run wails3 task ios:run. Go recompilation is incremental, so only changed packages rebuild.

</Steps>

Open in Xcode (optional)

bash
wails3 task ios:xcode

This opens build/ios/ in Xcode. You can use Xcode for device deployment, advanced profiling, or managing provisioning profiles. Wails regenerates the Xcode project on each build, so don't modify the generated files directly.

</TabItem> <TabItem label="Android Emulator" icon="seti:android">

Requirements

You need the Android SDK, NDK, and a JDK. The easiest way is Android Studio, or the command-line tools:

<Steps>
  1. Install the Android command-line tools

    Download from developer.android.com/studio#command-line-tools-only, unzip to ~/android-sdk/cmdline-tools/latest/.

  2. Install SDK components

    bash
    sdkmanager "platform-tools" \
               "platforms;android-35" \
               "build-tools;35.0.0" \
               "ndk;26.3.11579264" \
               "emulator" \
               "system-images;android-35;google_apis;arm64-v8a"
    
  3. Create an emulator

    bash
    avdmanager create avd \
      --name wails \
      --package "system-images;android-35;google_apis;arm64-v8a" \
      --device pixel_7
    
  4. Set environment variables

    Add to ~/.zshrc or ~/.bashrc:

    bash
    export ANDROID_HOME=~/android-sdk
    export ANDROID_SDK_ROOT=~/android-sdk
    export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin
    

    Reload: source ~/.zshrc

  5. Install a JDK

    bash
    # macOS
    brew install openjdk@21
    export JAVA_HOME=$(brew --prefix openjdk@21)
    
    # Ubuntu/Debian
    sudo apt install openjdk-21-jdk
    export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
    
    # Windows (scoop)
    scoop install openjdk21
    
</Steps>

Run wails3 doctor to confirm everything is found.

Run on the Emulator

<Steps>
  1. Launch the app

    bash
    wails3 task android:run
    

    On first run this:

    • Boots the emulator if none is running
    • Generates bindings and builds the frontend
    • Compiles your Go code to libwails.so via the NDK cross-compiler
    • Assembles a debug APK with Gradle
    • Installs and launches it on the emulator
    <Aside type="tip"> The first build downloads Gradle and compiles the NDK toolchain — expect 5–10 minutes. Subsequent builds are incremental and take under a minute. </Aside>
  2. Stream logs

    In a separate terminal:

    bash
    wails3 task android:logs
    

    This runs adb logcat filtered to your app. fmt.Println output appears here.

  3. Inspect the WebView

    Open Chrome and navigate to chrome://inspect. Your app's WebView appears under Remote Target — click inspect to open DevTools.

  4. Make a change

    Edit any file and re-run wails3 task android:run. Gradle's incremental build means only changed code recompiles.

</Steps> </TabItem> </Tabs>

Understanding what happened

Your main.go didn't change at all. Wails handled everything:

  • Build systemTaskfile.yml in your project contains ios:* and android:* tasks that drive the platform-specific toolchain.
  • Go cross-compilationGOOS=ios or GOOS=android with the appropriate GOARCH and sysroot.
  • Native host — a generated Xcode project (iOS) or Gradle project (Android) that embeds your compiled Go code and hosts the WebView.
  • Asset serving — your frontend/dist/ is embedded in the Go binary and served in-process. No localhost server is needed.

Make your app mobile-aware

Your app already works, but it looks like a desktop app on a phone screen. A few small changes make a big difference.

Responsive CSS

Mobile screens are narrower and use different input patterns. In frontend/public/style.css (or equivalent):

css
/* Prevent horizontal scrolling */
body {
  overflow-x: hidden;
}

/* Touch-friendly tap targets */
button {
  min-height: 44px;
  min-width: 44px;
}

/* Respect the iOS safe area (notch, home indicator) */
body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

Detect the platform in Go

Use build tags to add platform-specific behaviour without cluttering shared code.

Create mobile_ios.go for iOS-only code:

go
//go:build ios

package main

import "github.com/wailsapp/wails/v3/pkg/application"

func platformOptions() application.IOSOptions {
    return application.IOSOptions{
        DisableBounce: true,
    }
}

Create mobile_android.go for Android-only code:

go
//go:build android

package main

import "github.com/wailsapp/wails/v3/pkg/application"

func platformOptions() application.AndroidOptions {
    return application.AndroidOptions{}
}

Create mobile_desktop.go as a stub so the shared code compiles on desktop too:

go
//go:build !ios && !android

package main

type mobileOptions struct{}

func platformOptions() mobileOptions { return mobileOptions{} }

Detect the platform in JavaScript and gate mobile-only UI

The IOS.* and Android.* runtime objects only exist on their respective platforms. Calling them on desktop throws. The right pattern — used by the Kitchen Sink — is to detect the platform once and hide mobile-only controls entirely:

javascript
// Detect platform from the bridge the host injects into the WebView
const platform = (() => {
  if (typeof window.wails?.platform === 'function') return window.wails.platform(); // Android
  if (window.webkit?.messageHandlers?.external) return 'ios';
  return 'desktop';
})();

const isIOS     = platform === 'ios';
const isAndroid = platform === 'android';
const isMobile  = isIOS || isAndroid;

// Hide any element marked as mobile-only
document.querySelectorAll('.mobile-only').forEach(el => {
  el.style.display = isMobile ? '' : 'none';
});

Then in your HTML:

html
<section class="mobile-only">
  <button id="btnHaptic">Haptic feedback</button>
</section>

This way mobile-only buttons are never rendered on desktop, and you never need to guard every individual call with an if (isMobile) check.

On the Go side, pair this with a build-tag stub so the event handlers are only registered on the platforms that need them:

go
//go:build !ios && !android

package main

import "github.com/wailsapp/wails/v3/pkg/application"

// No-op on desktop — mobile tabs are hidden in the frontend so these
// events are never emitted.
func registerNativeFeatures(app *application.App) {}
go
//go:build ios

package main

import "github.com/wailsapp/wails/v3/pkg/application"

func registerNativeFeatures(app *application.App) {
    app.Event.On("common:haptic", func(e *application.CustomEvent) {
        // only compiled and called on iOS
        application.IOS.Haptic("medium")
    })
    // ... other handlers
}

This is the exact pattern the Kitchen Sink uses — see native_features_stub.go, native_features_ios.go, and native_features_android.go.

<Aside type="note" title="Native feature API & event naming"> Two conventions are worth knowing:
  • Go-side native features use platform managers. Call them through the application.IOS.* and application.Android.* singletons — for example application.IOS.Haptic("medium") or application.Android.Share(payload). Each manager only exists on its own platform, so its calls live in //go:build ios / //go:build android files.
  • Events are namespaced by reach. Anything both platforms understand uses the common:* prefix (common:haptic, common:location, …); events that only one platform can produce or handle use ios:* or android:* (for example ios:backgroundTask, android:foregroundService). Because almost every mobile feature is shared, your frontend keeps a single listener per event under common:*.
</Aside>

Add haptic feedback (iOS)

javascript
import { IOS } from '@wailsio/runtime';

async function onButtonTap() {
  if (isIOS) {
    await IOS.Haptics.Impact({ style: 'medium' });
  }
  // ... rest of your handler
}

Add vibration (Android)

javascript
import { Android } from '@wailsio/runtime';

async function onButtonTap() {
  if (isAndroid) {
    await Android.Haptics.Vibrate(50); // 50ms
  }
}

Build for production

<Tabs syncKey="mobile-platform"> <TabItem label="iOS" icon="apple">

Simulator build (for testing on simulator, no signing needed):

bash
wails3 task ios:package
wails3 task ios:deploy-simulator

Device build (requires a signing identity and provisioning profile):

bash
wails3 task ios:package \
  IOS_PLATFORM=device \
  CODESIGN_IDENTITY="Apple Development: You (TEAMID)" \
  PROVISIONING_PROFILE=path/to/profile.mobileprovision

wails3 task ios:deploy-device   # installs via xcrun devicectl

Distribution IPA (for App Store or TestFlight):

bash
wails3 task ios:package:ipa IOS_PLATFORM=device \
  CODESIGN_IDENTITY="..." \
  PROVISIONING_PROFILE=path/to/distribution.mobileprovision
<Aside type="tip"> For App Store Connect uploads, use `wails3 task ios:xcode` and let Xcode manage signing and archiving — it handles the complexity of certificates, profiles and notarization automatically. </Aside> </TabItem> <TabItem label="Android" icon="seti:android">

Debug APK (signed with the Android debug keystore, installs directly):

bash
wails3 task android:package
wails3 task android:deploy-emulator

Release APK (signed with your own keystore):

bash
ANDROID_KEYSTORE_FILE=/path/to/release.jks \
ANDROID_KEYSTORE_PASSWORD=yourpassword \
ANDROID_KEY_ALIAS=youralias \
ANDROID_KEY_PASSWORD=yourkeypassword \
  wails3 task android:package

Universal APK (arm64 + x86_64 in one file):

bash
wails3 task android:package:fat
<Aside type="tip"> For Play Store uploads, produce an `.aab` (Android App Bundle) instead of an APK — open `build/android/` in Android Studio and use **Build → Generate Signed Bundle / APK**. </Aside> </TabItem> </Tabs>

Troubleshooting

wails3 task ios:run fails with "no iOS SDKs found"

Full Xcode must be installed and selected:

bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcode-select -p  # should print the Xcode path

wails3 task android:run fails with "SDK not found"

Ensure ANDROID_HOME is set and exported. Verify with:

bash
echo $ANDROID_HOME
ls $ANDROID_HOME/platform-tools/adb

Simulator doesn't boot

List available simulators and boot one manually:

bash
xcrun simctl list devices available
xcrun simctl boot "iPhone 16"

chrome://inspect shows no targets

The WebView must be in debug mode (the default for android:run). Make sure you're running a debug build, not a production one. Also confirm adb devices shows the emulator as connected.

Safe area insets not applied

Make sure your HTML includes the viewport meta tag:

html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

Explore the Kitchen Sink

Once your first app is running, the Kitchen Sink example is the fastest way to learn what else is possible. It's a complete Wails app that runs on iOS, Android and desktop from a single codebase, covering haptics, geolocation, biometrics, local notifications, secure storage, and more:

bash
git clone https://github.com/wailsapp/wails.git
cd wails/v3/examples/mobile

wails3 task ios:run        # iOS Simulator
wails3 task android:run    # Android Emulator
wails3 task run            # Desktop

Browse the source at v3/examples/mobile — the native_features_ios.go and native_features_android.go files are particularly useful as copy-paste starting points for platform-specific features.

What's next

<CardGrid> <Card title="iOS Guide" icon="apple"> Full reference: configuration options, native tabs, WKWebView toggles, device builds, signing.
[iOS Guide →](/guides/mobile/ios)
</Card> <Card title="Android Guide" icon="seti:android"> Full reference: configuration, toasts, Play Store packaging, NDK details.
[Android Guide →](/guides/mobile/android)
</Card> <Card title="Kitchen Sink source" icon="open-book"> Haptics, geolocation, biometrics, notifications, secure storage — all in one runnable app.
[View on GitHub →](https://github.com/wailsapp/wails/tree/master/v3/examples/mobile)
</Card> </CardGrid>