Overview

When you build with Applin™, your app will have two parts:

  1. A frontend that fetches pages from the backend and displays them to the user. To make a frontend, copy an example app and change its name, icon, and server URL.
  2. A backend server that handles requests for pages and performs actions triggered by the user. To make a backend, use an HTTP server (like Ruby on Rails), import an Applin library, and add request handlers that process user-entered data and return page definitions.

Applin is like ReactNative without JavaScript.

Server-Driven UI

Applin is a Server-Driven User Interface (SDUI). This means that the server code defines app UI content, layout, and behavior.

Traditional app architecture: Traditional Architecture

Applin app architecture: Traditional Architecture

Benefits for you as an app maker:

  • Frontend code is eliminated. You need to learn, write, and maintain only the backend code. For example, you can use Rails to build and release an iOS app without help from an iOS developer.
  • When you deploy the backend, the frontend updates instantly.
    • You can delete old APIs right away, since there are no old versions of the app in use.
    • You can release features and fix bugs immediately, without waiting for app review.
    • When things go wrong, you can immediately roll back your app frontend & backend together.
  • Backend tests can exercise UI flows and backend code together. These run on normal test runners, not expensive macOS test runners.
  • Get support with GitHub Issues. You can easily find answers.
  • The source code is public. You can dig into the code and see how things work.
  • PRs are welcome! If you need a bug fixed ASAP, you can fix it and upstream your fix. No need to maintain a fork for more than one release.
  • To support a new version of iOS, just rebuild with the latest version of the Applin frontend.

Benefits for your users:

  • Users always have the latest version of the app.
  • The app starts and runs very fast.
  • It uses little battery power. Processing happens on the server.
  • It runs on the cheapest devices. The SDUI boilerplate uses little RAM and CPU.
  • The app downloads quickly and takes very little space on their device (1.2MB).

Downsides:

  • Custom UI elements are not easy to make in Applin.
  • Some bad UI designs are impossible to make in Applin. This is a benefit, depending on your perspective.
  • Applin is new. All new software has bugs.
  • Styling support is still under development.
  • The Android frontend is still under development.
  • The library of UI elements is still small.
  • Applin costs money.

Frontend

When the frontend starts up, it makes an HTTP request to the server for the home page. It expects to get a JSON response with content-type application/vnd.applin_response. It parses the response into a page definition. Then it dynamically creates native iOS (UIKit) objects and composes them to match the page definition. Later, if the page updates, the frontend gets a new page definition and smoothly updates the page.

Currently, there is one frontend: iOS Frontend.

Backend

The backend of an Applin app is an HTTP server. It handles these kinds of requests:

  • GET requests for pages
  • POST requests for pages with user-entered data (text boxes, checkboxes, etc.)
  • POST requests triggered by the user (buttons, camera, etc.)

Applin provides libraries for building backend servers. See Backend Libraries.

Applin is not a no-code app builder. You must write backend code.

Frontend

Every Applin app must get page definitions from the backend server and display them. For details, see Frontend-Backend Protocol.

We provide frontend example apps for various platforms. You can copy one of these example apps and change the server URL to point to your server.

iOS Frontend

Applin's iOS frontend is in the ApplinIos library. To use it, we create a boilerplate iOS app and have it call the library on startup.

You can try the iOS frontend right now. Install the Applin Tester app from the App Store and connect to one of the demo backends. See Manual Testing.

Make a New App

  1. Download https://github.com/leonhard-llc/applin-ios-example
  2. Open the project in XCode
  3. Xcode's left-hand panel is a tab view displaying "navigators". Display the Project Navigator by clicking the left-most icon or selecting View > Navigators > Project from the menu.
  4. Change your app's Bundle Identifier
  5. Change your app's name
    • Click "Applin Example" at the top of Project Navigator. Click it again to change it into a text edit box.
    • Enter your app's new name and press ENTER.
    • "Rename project content items?" appears. Click "Rename".
    • Rename the "Applin Example" under "TARGETS"
    • Rename the "Applin Example" folder to match your app's name.
    • Select your app under "TARGETS", click the "Build Settings" tab, and change "Packaging > Info.plist File" from Applin Example/Info.plist to the new path of the file.
  6. Add your app icon to Assets > AppIcon
  7. Replace logo.png with your app's logo
    • Info.plist > Launch Screen > Image Name tells UIKit to generate a launch screen that displays logo.png in the center. It sizes the image by mapping 1 image pixel to 1 device-independent pixel. To change the logo's size on the screen, use an image editor to change its resolution.
    • To get more control over the launch screen, use a Storyboard Launch Screen.
  8. Edit Main.swift
    • Check the server URLs
      • urlForSimulatorBuilds
      • urlForDebugBuilds (for testing the app on your own devices)
      • licenseKey (used by Release builds, contains prod server URL)
    • Customize firstStartupPage
    • Update your support channels or set them to nil. These are used in StaticPages.support.
      • statusPageUrl
      • supportChatUrl
      • supportEmailAddress
      • supportSmsTel
  9. Replace terms.txt and privacy.txt

Build and Run

Build your app and run it in Simulator:

  1. Use the device selector in the XCode bar and select an item under "iOS Simulators".
    • To change the available simulated devices, click the menu item "Window > Devices and Simulators".
  2. Click the "Start the active scheme" button which looks like a triangle

screenshot

The first time XCode launches an app, it starts very slowly. When you launch an app by tapping the icon, it starts instantly.

Note: Simulator has some slightly differences from real devices. See https://developer.apple.com/documentation/xcode/devices-and-simulator/. Also, Applin has a rendering bug (applin-ios#1) that only appears in Simulator.

If you try to run your app on a physical device and it fails with a signing error, you must complete these steps:

  1. Create an Apple Developer account
  2. Pay Apple their annual fee
  3. Register your device to your Apple Developer account

Note: Release builds require a valid Applin license key. See Releasing.

Android Frontend

Applin's Android frontend is under development and is not yet ready to use.

To get updates on Applin for Android and other announcements, add yourself to https://groups.google.com/g/applin-announce.

Backend Libraries

Every Applin app must have an HTTP server that handles requests from the frontend. For details, see Frontend-Backend Protocol.

We provide libraries to help you make your backend server.

Ruby on Rails

You can write an Applin backend in Rails.

Get Started

  1. Learn https://rubyonrails.org/
  2. Create a new Rails project
  3. Add https://rubygems.org/gems/applin-rails

Routes

The Applin frontend sends GET to load a new page.

When refreshing a page, it sends POST if the page has any user-input widgets. The request body is a JSON object containing the page's variables. So all pages with user-input widgets need to handle POST.

When a page has no user-input widgets, the frontend uses GET to refresh the page.

The rpc action also sends POST with the page's variables.

For details, see Frontend-Backend Protocol.

# config/routes.rb
Rails.application.routes.draw do
  # https://guides.rubyonrails.org/routing.html
  get "/healthz", to: proc { [200, {}, ["success"]] }
  
  # Requests for "/" go to HomeController.index.
  root "home#index"
  
  # Frontend sends POST when refreshing a page with user-input widgets. 
  get "/login_page", to: "login#login_page"
  post "/login_page", to: "login#login_page"
  
  # Frontend sends POST for RPC action.
  post "/login", to: "login#login"
end

Controllers

Your server must require "applin/rails" which adds the applin format. Then Rails can recognize Applin requests when it checks the Accept header.

Applin is not affected by CSRF attacks. Rails has automatic CSRF protection for POST requests. We call protect_from_forgery to disable it for Applin requests:

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  # Prevent error "ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.)"
  protect_from_forgery with: :exception, if: -> { !request.format.applin? }
end

Rails automatically parses the POST body JSON object and makes it available in the params object. To learn how to handle request parameters, see the Ruby on Rails Guides. Also see Submitting Data.

Views

Your server can require "applin" and then your views can call Applin module functions to create pages and widgets.

We recommend using the jbuilder JSON template library.

To display a page in the frontend, your server must return a response with the proper content-type header value. If your view template filename ends in .applin.jbuilder then Rails will:

  1. use the JBuilder templating engine to process the template into a response body
  2. add the Applin content-type header to the response

The response body must be a JSON object with a page entry.

# app/views/home/index.applin.jbuilder 
json.page Applin::nav_page(title: "Home Page", poll_seconds: 30) {
  Applin::scroll {
    Applin::column(widgets: [
      Applin::text("text")
    ])
  }
}

Controller-Only

You have the option of defining your pages inside the controller, without view templates.

Use Applin::Rails::send_page and the various functions in the Applin module.

# app/controllers/home_controller.rb
require "applin"
require "applin/rails"

class HomeController < ApplicationController
  include ::Applin
  include ::Applin::Rails
  skip_forgery_protection

  def index
    send_page nav_page(title: "Home Page", poll_seconds: 30) {
      scroll {
        column(widgets: [
          text("text")
        ])
      }
    }
  end
end

Example

https://github.com/leonhard-llc/applin-rails-demo

Rust

Are you considering using Rust? Read this article: Why Rust in Production?.

https://crates.io/crates/applin - We provide this library to help your Rust HTTP server produce Applin pages.

Rust has many HTTP server libraries. Please let us know which one to add next: Feature Requests.

Servlin

You can use the https://crates.io/crates/servlin HTTP server for your Applin backend.

  1. Enable the servlin feature of the applin crate.
  2. Use the applin::applin_response function to convert your page definition to a servlin::Response struct with the proper content-type and JSON body. Example:
    #![allow(unused)]
    fn main() {
    pub const HELLO_PAGE_KEY: &str = "/hello";
    pub fn hello_page() -> Response {
        applin_response(nav_page("Hello", scroll(form(text("Hello World!")))))
            .unwrap()
    }
    }
  3. Remember to handle GET and POST for pages that have input widgets (variables). Example:
    #![allow(unused)]
    fn main() {
    ("GET" | "POST", account::NEW_ACCOUNT_PAGE_KEY) => Ok(account::new_account_page()),
    }

Example: https://github.com/leonhard-llc/applin-rust-demo

Pages

Page URL

Every page has a relative URL. The app's home page is always /.

See Protocol - Pages and URLs.

Page Stack

The frontend keeps a stack of pages. The top-most page is visible to the user.

The first time the app starts, it starts with only ApplinConfig.showPageOnFirstStartup url on the stack.

Widget actions can push or pop pages on the stack. The stack cannot become empty.

The frontend saves the page stack. If the OS stops the app, the frontend restores the page stack when the user opens the app again. The frontend also saves and restores user-entered data (text fields, checkboxes, etc.). This means the user can continue using the app where they left-off.

If the user forcefully terminates the app, the frontend erases the page stack. The next time the app starts, it shows the default page:

  • If the server previously set a "session" cookie, then it starts with page /.
  • Otherwise, it starts with the value of ApplinConfig.showPageOnFirstStartup.

Page Variables

Some widgets allow the user to enter data or select options. We call them input widgets. For example, text_field and checkbox are input widgets.

Every input widget has a var_name attribute, for variable name.

When the page refreshes or performs an RPC action, the frontend sends the page's variables to the backend in a JSON object HTTP POST body. See Protocol.

The frontend saves variables values in a single dictionary for all pages. Widgets on different pages can use the same var_name and they will show the same data.

Static Pages

Static pages are defined in frontend code.

Refresh

The user can refresh the page by:

The page refreshes smoothly.

# Ruby
nav_page(title: "Inert") {
  scroll {
    form(widgets: [
      text("This page updates when you load or refresh it (pull to refresh)."),
      text(Time.new.strftime("%Y-%m-%d %H:%M:%S")),
    ])
  }
}
#![allow(unused)]
fn main() {
nav_page(
    "Inert",
    scroll(form((
        text("This page updates when you load or refresh it (pull to refresh)."),
        text(format!("{:?}", SystemTime::now())),
    ))),
)
}

Polling

Pages with the poll_seconds option will automatically refresh. The page refreshes smoothly, even while the user is typing on the keyboard.

# Ruby
nav_page(title: "Polled", poll_seconds: 2) {
  scroll {
    form(widgets: [
      text("This page updates automatically every 2 seconds."),
      text(Time.new.strftime("%Y-%m-%d %H:%M:%S")),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Polled",
    scroll(form((
        text("This page updates automatically every 2 seconds."),
        text(format!("{:?}", SystemTime::now())),
    ))),
)
.with_poll(2),
}

Nav Page

Nav pages show a navigation bar at the top with the page title. They can also show a "Back" button.

Attributes:

  • widget: Widget
    • Required
    • The widget to display on the page
  • title: "Page One"
    • Required
    • Displayed in the navigation bar
    • Available to analytics tools through UIKit UIViewController.title
  • poll_seconds: n
    • Page silently refreshes every n seconds, when the app is in the foreground.
    • Page silently refreshes when the user switches to the app.
  • start: widget
    • Replaces the default back button with a custom back_button.
    • Set this to the empty widget to remove the back button.
    • On iOS, setting this widget disables the swipe-back gesture. This is because of bugs in Apple's UIKit.
  • ephemeral: true
    • For static pages only.
    • When the user terminates the app and starts it again, do not restore this page or any subsequent pages in the stack. This is useful for error pages.

Example

screenshot

# Ruby
nav_page(title: "Nav Page") {
  scroll(form(widgets: [
    text("text")
  ]))
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Nav Page", scroll(form((
    text("text"),
))))
}

Home Page

When a nav page is the only page on the stack, it doesn't show a Back button.

screenshot

# Ruby
nav_page(title: "Home Page", poll_seconds: 30) {
  scroll {
    column(widgets: [
      text("text")
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Home Page", scroll(form((
    text("text"),
))))
.with_poll(30)
}

Removed Back Button

To remove the back button, set the nav page's start attribute to the empty widget.

The user may become frustrated. See section 3 "User control and freedom" of Nielsen Norman's 10 Usability Heuristics for User Interface Design.

screenshot

# Ruby
nav_page(title: "Removed Back Button", start: empty) {
  scroll {
    column(widgets: [
      text("text"),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Removed Back Button", scroll(column((
    text("text"),
))))
.with_empty_start()
}

Plain Page

Plain pages are good for the home screen, since they do not display title bar or "Back" button.

Attributes:

  • widget: Widget
    • Required
    • The widget to display on the page
  • title: "Page One"
    • Available to analytics tools through UIKit UIViewController.title
  • poll_seconds: n
    • Page silently refreshes every n seconds, when the app is in the foreground.
    • Page silently refreshes when the user switches to the app.
  • ephemeral: true
    • For static pages only.
    • When the user terminates the app and starts it again, do not restore this page or any subsequent pages in the stack. This is useful for error pages.

Example

screenshot

# Ruby
plain_page(title: "Home Page", poll_seconds: 30) {
  scroll {
    column(widgets: [
      text("text"),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Home Page", scroll(column((
    text("text"),
))))
.with_poll(30)
}

Widgets

A widget is a UI element like a button or text label. Some widgets are used for layout, like column and form.

Do you need a widget that's not here? Please let us know: Feature Requests.

back_button Widget

Use a back button widget to customize the back button of a nav_page. Assign the widget to the start attribute of the nav page.

Back button widgets cannot appear anywhere else.

Attributes:

  • actions: [action]
    • Required
    • A list of actions to perform when the user taps the back button.
    • When the list is empty, the button is disabled.

RPC Example

In this example, the back button performs an rpc action.

If the RPC fails, the page will not pop. The user may become frustrated and stuck. See section 3 "User control and freedom" of Nielsen Norman's 10 Usability Heuristics for User Interface Design.

screenshot

# Ruby
nav_page(
    title: "Edit Note",
    start: back_button(actions: [rpc("/save_note"), pop]),
  ) {
  scroll {
    column(widgets: [
      textfield(var_name: "note"),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Edit Note", scroll(column(textfield("note"))))
.with_start(back_button([rpc("/save_note"), pop()]))
}

Disabled

To disable the back button, set the actions to an empty list.

The user may become frustrated. See section 3 "User control and freedom" of Nielsen Norman's 10 Usability Heuristics for User Interface Design.

screenshot

# Ruby
nav_page(title: "Disabled Back Button", start: back_button(actions: [])) {
  scroll {
    column(widgets: [
      text("text"),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page("Removed Back Button", scroll(form(text("text"))))
.with_start(back_button([]))
}

button Widget

The user can tap a button widget to perform a list of actions.

Attributes:

  • text: "My Button"
    • Required
    • Text to display on the button
    • Must contain a non-whitespace symbol
  • actions: [action]
    • A list of actions to perform when the user taps the button
    • When the list is empty, the button is disabled.

Example

screenshot screenshot

# Ruby
button(text: "Button", actions: [rpc("/add_item"), pop])
#![allow(unused)]
fn main() {
// Rust
button("Button", [rpc("/add_item"), pop()])
}

Many Words

screenshot

# Ruby
button(
  text: "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
  actions: [rpc("/add_item"), pop]),
)

Long Word

screenshot

# Ruby
button(
  text: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
  actions: [rpc("/add_item"), pop]),
)

Disabled

screenshot screenshot

# Ruby
button(text: "Disabled Button", actions: [])
#![allow(unused)]
fn main() {
// Rust
button("Disabled Button", [])
}

checkbox Widget

A checkbox widget to displays the value of a boolean (true or false) variable. The user can tap the widget to toggle the value of the variable.

Attributes:

  • var_name: "var1
    • Variable name
    • Required
  • actions: [action]
    • A list of actions to perform when the user taps the button
    • When the list is empty, the button is disabled.
  • initial_bool: true
    • The initial value of the variable, if the variable is unset.
  • poll_delay_ms: 0
    • Poll (update) the page immediately when the user changes the checkbox state.
    • Use this for search boxes.
  • poll_delay_ms: N
    • Poll (update) the page after the user changes the checkbox state and N milliseconds passes.
    • Use this to validate selections and show warnings.
  • text: "Label1
    • A label to display next to the checkbox.

Example

screenshot screenshot

# Ruby
nav_page(title: "Checkbox") {
  scroll {
    form(widgets: [
      checkbox(text: "Option 1", var_name: "option_1"),
      checkbox(text: "Option 2", var_name: "option_2", initial_bool: true),
      checkbox(text: "Option 3", var_name: "option_3", rpc: "/ok"),
      checkbox(text: "", var_name: "no_label"),
      checkbox(
        text: "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
        var_name: "many_words",
      ),
      checkbox(
        text: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
        var_name: "long_word",
      ),
      checkbox(text: "Polls page", var_name: "poll", actions: [Applin::poll]),
      checkbox(
        text: "Polls page after 1 second delay",
        var_name: "poll_delay",
        poll_delay_ms: 1000,
      ),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Checkbox",
    scroll(form((
        checkbox("option_1").with_text("Option 1"),
        checkbox("option_2").with_text("Option 2").with_initial_bool(true),
        checkbox("option_3").with_text("Option 3").with_rpc("/ok"),
        checkbox("no_label"),
        checkbox("many_words")
            .with_text("MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM"),
        checkbox("long_word")
            .with_text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"),
        checkbox("polls")
            .with_text("Polls page")
            .with_poll_delay(Duration::ZERO),
        checkbox("poll_delay")
            .with_text("Polls page after 1 second delay")
            .with_poll_delay(Duration::from_secs(1)),
    ))),
)
}

column Widget

A column arranges its child widgets in a vertical column.

Attributes:

  • widgets: [Widget]
    • Required
    • Widgets to display, in order starting with the top
    • May be empty
  • alignment: "start"
    • Align widgets with the left side of the column widget.
    • Default
  • alignment: "center"
    • Align widgets with the center of the column widget.
  • alignment: "end"
    • Align widgets with the right side of the column widget.
  • spacing: n
    • Separate widgets by N device-independent pixels

Example

screenshot

# Ruby
nav_page(title: "Column") {
  scroll {
    column(widgets: [
      text("text1"),
      text("text2"),
      text("text3"),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Column",
    scroll(column((text("text1"), text("text2"), text("text3")))),
)
}

Other Options

screenshot

# Ruby
nav_page(title: "Column") {
  scroll {
    column(widgets: [
      form_section(title: "Default", widgets: [
        column(widgets: [
          text("text"),
          text("text"),
        ])
      ]),
      form_section(title: "Start", widgets: [
        column(align: ALIGN_START, widgets: [
          text("text"),
          text("text"),
        ])
      ]),
      form_section(title: "Center", widgets: [
        column(align: ALIGN_CENTER, widgets: [
          text("text"),
          text("text"),
        ])
      ]),
      form_section(title: "End", widgets: [
        column(align: ALIGN_END, widgets: [
          text("text"),
          text("text"),
        ])
      ]),
      form_section(title: "Spacing", widgets: [
        column(spacing: 16.0, widgets: [
          text("text"),
          text("text"),
        ])
      ]),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Column",
    scroll(column((
        form_section("Default", column((text("text"), text("text")))),
        form_section(
            "Start",
            column((text("text"), text("text"))).with_alignment(HAlignment::Start),
        ),
        form_section(
            "Center",
            column((text("text"), text("text"))).with_alignment(HAlignment::Center),
        ),
        form_section(
            "End",
            column((text("text"), text("text"))).with_alignment(HAlignment::End),
        ),
        form_section(
            "Spacing",
            column((text("text"), text("text"))).with_spacing(16),
        ),
    ))),
)
}

empty Widget

The empty widget is invisible and takes no space.

Example

screenshot

# Ruby
nav_page(title: "Empty Page") { empty }
#![allow(unused)]
fn main() {
// Rust
nav_page("Empty Page", empty())
}

error_text Widget

An error_text widget displays a message to the user with a warning icon.

Attributes:

  • text: "Message1
    • Required
    • A message to display.

Example

screenshot screenshot

# Ruby
error_text("Error Message")
#![allow(unused)]
fn main() {
// Rust
error_text("Error Message")
}

form Widget

A form arranges its child widgets in a vertical column, separated by horizontal lines.

Attributes:

  • widgets: [Widget]
    • Required
    • Widgets to display, in order starting with the top
    • May be empty

Example

screenshot screenshot

# Ruby
nav_page(title: "Form") {
  scroll {
    form(widgets: [
      text("text"),
      checkbox(var_name: "box1", text: "Box 1"),
      form_button(text: "Next", actions: [rpc("/next"), push("/page2")]),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Form",
    scroll(form((
        text("text"),
        checkbox("box1").with_text("Box 1"),
        form_button("Next", [rpc("/next"), push("/page2")]),
    ))),
)
}

form_button Widget

The user can tap a form_button widget to perform a list of actions.

Attributes:

  • text: "My Button"
    • Required
    • Text to display on the button
    • Must contain a non-whitespace symbol
  • actions: [action]
    • A list of actions to perform when the user taps the back button
    • When the list is empty, the button is disabled.

Example

screenshot screenshot

# Ruby
nav_page(title: "Form Button") {
  scroll {
    form(widgets: [
      form_button(text: "Button", actions: [rpc("/add_item"), pop]),
      form_button(
        text: "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
        actions: [rpc("/add_item"), pop],
      ),
      form_button(
        text: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
        actions: [rpc("/add_item"), pop],
      ),
      form_button(text: "Disabled", actions: []),
      form_button(
        text: "Start Aligned",
        actions: [rpc("/add_item"), pop],
        align: ALIGN_START,
        ),
      form_button(
        text: "Center Aligned",
        actions: [rpc("/add_item"), pop],
        align: ALIGN_CENTER,
      ),
      form_button(
        text: "End Aligned",
        actions: [rpc("/add_item"), pop],
        align: ALIGN_END,
      ),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Form Button",
    scroll(form((
        form_button("Button", [rpc("/add_item"), pop()]),
        form_button(
            "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
            [rpc("/add_item"), pop()],
        ),
        form_button(
            "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
            [rpc("/add_item"), pop()],
        ),
        form_button("Disabled", []),
        form_button("Start Aligned", [rpc("/add_item"), pop()]).with_align(HAlignment::Start),
        form_button("Center Aligned", [rpc("/add_item"), pop()]).with_align(HAlignment::Center),
        form_button("End Aligned", [rpc("/add_item"), pop()]).with_align(HAlignment::End),
    ))),
)
}

form_section Widget

A form_section widget shows a labeled grouping of widgets.

Attributes:

  • widgets: [Widget]
    • Required
    • A list of widgets to display
  • title: "Section 1"
    • A text label

Example

screenshot screenshot

# Ruby
nav_page(title: "Form Section") {
  scroll {
    column(widgets: [
      form_section(title: "Section 1", widgets: [text("text"), text("text")]),
      form_section(title: "Section 2", widgets: [text("text"), text("text")]),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Form Section",
    scroll(column((
        form_section("Section 1", (text("text"), text("text"))),
        form_section("Section 2", (text("text"), text("text"))),
    ))),
)
}

grouped_row_table Widget

A grouped_row_table displays widgets in a grid. It can display separate groups of rows.

Attributes:

  • widgets: [Widget]
    • Required
    • A list of widgets to display
  • spacing: n
    • Separate widgets by N device-independent pixels

Example

screenshot screenshot

# Ruby
nav_page(title: "Grouped Row Table") {
  scroll {
    form(widgets: [
      form_section(title: "Two Groups", widgets: [
        grouped_row_table(spacing: 8, row_groups: [
          [
            [text("A1"), text("B1"), text("C1")],
            [text("A2"), text("B2"), nil],
          ],
          [
            [text("One"), nil, text("Three")],
          ],
        ]),
      ]),
      form_section(title: "Long text", widgets: [
        grouped_row_table(spacing: 8, row_groups: [
          [
            [
              text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"),
              text("MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM"),
            ],
            [text("A2"), text("B2")],
          ],
        ]),
      ]),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Grouped Row Table",
    scroll(form((
        form_section(
            "Two Groups",
            grouped_row_table((
                (
                    (text("A1"), text("B1"), text("C1")),
                    (text("A2"), text("B2"), None),
                ),
                ((text("One"), None, text("Three")),),
            ))
            .with_spacing(8),
        ),
        form_section(
            "Long text",
            grouped_row_table(((
                (
                    text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"),
                    text("MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM"),
                ),
                (text("A2"), text("B2")),
            ),))
            .with_spacing(8),
        ),
    ))),
)
}

image Widget

A image displays widgets in a grid. It can display separate groups of rows.

Attributes:

  • aspect_ratio: 4.0
    • Required
    • The aspect ratio of the image widget.
    • This is the width divided by the height.
  • url: "/image.png"
    • Required
    • URL of the image to fetch and display. This may be a relative URL like "/image.png" or an absolute URL on any server like "https://example.com/image.png".
    • The iOS frontend supports PNG and JPEG file formats.
  • disposition: "fit"
    • Display the image as large as possible while still being entirely visible.
  • disposition: "cover"
    • Expand the image so the entire widget is covered by the image, without stretching the image.
    • May cut off the edges of the image.
  • disposition: "stretch"
    • Make the image fill the widget, stretching it as needed.

Example

screenshot screenshot

# Ruby
nav_page(title: "Image") {
  scroll {
    form(widgets: [
      text("Fit"),
      image(
        aspect_ratio: 4.0,
        url: "/image.png",
        disposition: DISPOSITION_FIT,
      ),
      text("Cover"),
      image(
        aspect_ratio: 4.0,
        url: "/image.png",
        disposition: DISPOSITION_COVER,
      ),
      text("Stretch"),
      image(
        aspect_ratio: 4.0,
        url: "/image.png",
        disposition: DISPOSITION_STRETCH,
      ),
      text("Not found"),
      image(
        aspect_ratio: 4.0,
        url: "/nonexistent",
        disposition: DISPOSITION_FIT,
      ),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Image",
    scroll(form((
        text("Fit"),
        image(Disposition::Fit, 4.0, "/image.png"),
        text("Cover"),
        image(Disposition::Cover, 4.0, "/image.png"),
        text("Stretch"),
        image(Disposition::Stretch, 4.0, "/image.png"),
        text("Not found"),
        image(Disposition::Fit, 4.0, "/nonexistent"),
    ))),
)
}

last_error_text Widget

A last_error_text widget displays the text details of the most recent error.

Use this widget in static error detail pages.

nav_button Widget

The user can tap a nav_button widget to perform a list of actions.

The button has a chevron "⟩" symbol. Users expect the button to navigate to another page.

Attributes:

  • text: "Page 1"
    • Required
    • Text to display on the button
    • Must contain a non-whitespace symbol
  • sub_text: "Some info about Page 1."
    • Text to display under the main text.
  • actions: [action]
    • A list of actions to perform when the user taps the button
    • When the list is empty or missing, the button is disabled.
  • badge_text: "5"
    • Text to display in a badge on the button.
  • photo_url: "/image.png"
    • URL of an image to fetch and display. This may be a relative URL like "/image.png" or an absolute URL on any server like "https://example.com/image.png".
    • The iOS frontend supports PNG and JPEG file formats.

Example

screenshot screenshot

# Ruby
nav_page(title: "Nav Button") {
  scroll {
    form(widgets: [
      nav_button(text: "Page 1", actions: [push("/p1")]),
      nav_button(
        text: "Page 2",
        sub_text: "A very nice page",
        actions: [push("/p2")],
      ),
      nav_button(text: "Page 3", badge_text: "5", actions: [push("/p3")]),
      nav_button(
        text: "Page 4",
        badge_text: "123456789012345678901234567890",
        actions: [push("/p4")],
      ),
      nav_button(
        text: "Page 5",
        photo_url: "/image.png",
        actions: [push("/p5")],
      ),
      nav_button(
        text: "Page 6",
        photo_url: "/nonexistent.png",
        actions: [push("/p6")],
      ),
      nav_button(text: "Disabled", actions: []),
      nav_button(
        text: "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
        actions: [push("/mmmm_mmmm")],
      ),
      nav_button(
        text: "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
        actions: [push("/mmmmmmmm")],
      ),
    ])
  }
}
#![allow(unused)]
fn main() {
// Rust
nav_page(
    "Nav Button",
    scroll(form((
        nav_button("Page 1", [push("/p1")]),
        nav_button("Page 2", [push("/p2")]).with_sub_text("A very nice page"),
        nav_button("Page 3", [push("/p3")]).with_badge_text("5"),
        nav_button("Page 4", [push("/p4")]).with_badge_text("123456789012345678901234567890"),
        nav_button("Page 5", [push("/p5")]).with_photo_url(PLACEHOLDER_IMAGE_KEY),
        nav_button("Page 6", [push("/p6")]).with_photo_url("/nonexistent.png"),
        nav_button("Disabled", []),
        nav_button(
            "MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM",
            [push("/mmmm_mmmm")],
        ),
        nav_button(
            "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM",
            [push("/mmmmmmmm")],
        ),
    ))),
)
}

scroll Widget

A scroll Widget displays a child widget. When the child widget is taller than the available space, the user can drag on the widget to scroll it.

The user can also drag down to poll the page. This function is known as "pull to refresh".

Attributes:

  • widget: Widget
    • Required
    • The widget to display inside the scroll widget
  • pull_to_refresh: true
    • Set to false to disable the pull-to-refresh function.

Example

# Ruby
nav_page(title: "Inert") {
  scroll {
    form(widgets: [
      text("This page updates when you load or refresh it (pull to refresh)."),
      text(Time.new.strftime("%Y-%m-%d %H:%M:%S")),
    ])
  }
}
#![allow(unused)]
fn main() {
nav_page(
    "Inert",
    scroll(form((
        text("This page updates when you load or refresh it (pull to refresh)."),
        text(format!("{:?}", SystemTime::now())),
    ))),
)
}

table Widget

Coming soon...

text Widget

A text widget displays a message to the user.

Attributes:

  • text: "Message1
    • Required
    • A message to display.

Example

screenshot screenshot

# Ruby
nav_page(title: "Text") {
  scroll {
    form(widgets: [
      text("Hello world!"),
      text("MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM MMMM"),
      text("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"),
    ])
  }
}
#![allow(unused)]
fn main() {
text("Hello world!")
}

textfield Widget

A textfield widget to displays the value of a string variable. The user can tap the widget to edit the value.

Attributes:

  • var_name: "var1
    • Variable name
    • Required
  • initial_string: "initial text
    • The initial value of the variable, if the variable is unset.
  • label: "Label1
    • A label to display next to the textfield.
  • error: "Message1
    • A message to display next to the textfield, with an alert icon.
    • When this attribute is set, the textfield has a red border.
  • auto_capitalize: "names"
    • Automatically capitalize the first letter of each word the user types.
  • auto_capitalize: "sentences"
    • Automatically capitalize the first letter of each sentence the user types.
  • allow: "all"
    • Allow the user to enter any text and symbols
  • allow: "ascii"
    • Allow the user to enter only ASCII text and symbols.
    • This disallows emoji and non-English letters and characters.
  • allow: "email"
    • Allow the user to enter only text and symbols used in email addresses.
  • allow: "numbers"
    • Allow the user to enter only numbers.
  • allow: "tel"
    • Allow the user to enter symbols used in telephone numbers.
  • max_lines: N
    • Restrict the user from entering more than N lines of text.
  • poll_delay_ms: 0
    • Poll (update) the page immediately when the user changes the text.
    • Use this for search boxes.
  • poll_delay_ms: N
    • Poll (update) the page after the user changes the text and N milliseconds passes.
    • Use this to validate text input and show warnings.

Example

screenshot screenshot

# Ruby
nav_page(title: "Text Field") {
  scroll {
    form(widgets: [
      textfield(var_name: "field1", label: "Field 1"),
      textfield(var_name: "field2", label: "Field 2", error: "An error message."),
      textfield(var_name: "field3", label: "Field 3", initial_string: "initial text"),
      textfield(
        var_name: "mmmm",
        label: "Field 4",
        initial_string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      ),
      textfield(var_name: "field4", label: "F5", allow: ALLOW_NUMBERS),
      textfield(var_name: "field5", label: "F6", allow: ALLOW_ASCII),
      textfield(var_name: "field6", label: "F7", allow: ALLOW_TEL),
      textfield(var_name: "field7", label: "F8", allow: ALLOW_EMAIL),
      textfield(var_name: "field8", label: "F9", auto_capitalize: AUTO_CAPITALIZE_NAMES),
      textfield(var_name: "field8", label: "F10", auto_capitalize: AUTO_CAPITALIZE_SENTENCES),
      textfield(var_name: "field9", label: "F11", max_lines: 1),
      textfield(var_name: "field10", label: "F12", max_lines: 3),
      textfield(
        var_name: "field11",
        label: "Polls page on change",
        poll_delay_ms: 0,
      ),
      textfield(
        var_name: "field12",
        label: "Polls page after change and 1 second delay",
        poll_delay_ms: 1000,
      ),
    ])
  }
}
#![allow(unused)]
fn main() {
nav_page("Text Field", scroll(form((
  textfield("field1").with_label("Field 1"),
  textfield("field2").with_label("Field 2").with_error("An error message."),
  textfield("field3").with_label("Field 3").with_initial_string( "initial text"),
  textfield("mmmm")
  .with_label("Field 4")
  .with_initial_string( "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."),
  textfield("field4").with_label("Enter numbers").with_allow( Allow::Numbers),
  textfield("field5").with_label("Enter text").with_allow( Allow::Ascii),
  textfield("field6").with_label("Enter tel").with_allow( Allow::Tel),
  textfield("field7").with_label("Enter email").with_allow( Allow::Email),
  textfield("field8").with_label("Enter name").with_auto_capitalize(AutoCapitalize::Names),
  textfield("field8").with_label("Enter sentences").with_auto_capitalize(AutoCapitalize::Sentences),
  textfield("field9").with_label("Polls page on change").with_poll_delay(Duration::ZERO),
  textfield("field10")
    .with_label("Polls page after change and 1 second delay")
    .with_poll_delay(Duration::from_secs(1)),
))))
}

Actions

Add actions to button widgets and other widgets to let the user initiate actions in the app.

When the user taps a widget, the frontend performs the action list. It performs the actions in order. If it encounters an error while performing an action, it stops.

For example, when performing the action list ["rpc:/save", "pop"], if the RPC to /save returns an error, the frontend will not pop the page.

Do you need an action that's not here? Please let us know: Feature Requests.

launch_url Action

Use launch_url to launch a URL on the user's device. The URL can point to a webpage or another app.

Example URLs:

  • "https://www.example.com/support"
  • "mailto:support@example.com?subject=Support&body=Error%20E123" (subject and body are percent-encoded)
  • "tel:+12223334444
  • "sms:+12223334444

More info:

Note: Simulator doesn't have email or phone apps so mailto and tel links don't work.

Examples

# Ruby
launch_url("https://www.example.com/support")
#![allow(unused)]
fn main() {
// Rust
use applin::{button, launch_url};
button("Support", [launch_url("https://www.example.com/support")])
}

on_user_error_poll Action

Add on_user_error_poll to the submit button on a form so the form can show error messages on input widgets.

When the frontend executes an action list, and an action fails, it stops executing the list. If the action failed with an HTTP 403 or 422 response, and the list contains on_user_error_poll, then the frontend polls the page before displaying the error page.

Examples

# Ruby
on_user_error_poll
#![allow(unused)]
fn main() {
// Rust
use applin::{button, on_user_error_poll, rpc};
button("Signup", [rpc("/signup"), on_user_error_poll()])
}

poll Action

Use poll to update the page based on page data or backend data.

To execute the action, the frontend re-fetches the page from the server and then smoothly updates the displayed page to match the new version it received.

It shows the "Working" modal.

If the page has any variables, the frontend uses an HTTP POST and includes the variables and their values in a JSON object request body.

Examples

# Ruby
poll
#![allow(unused)]
fn main() {
// Rust
poll()
}

pop Action

Use pop to remove the current page from the stack.

The nav_page default Back button performs this action.

If the current page is the only page on the stack, this action displays an error.

Examples

# Ruby
pop
#![allow(unused)]
fn main() {
// Rust
pop()
}

push Action

Use push to add a page to the top of the stack, making it visible.

If the specified page is already visible, this action display an error.

Examples

# Ruby
push("/page2")
#![allow(unused)]
fn main() {
// Rust
push("/page2")
}

replace_all Action

Use replace_all to remove all pages on the stack and replace them with the specified page.

Login pages can use this to show the home page after login.

Examples

# Ruby
replace_all("/")
#![allow(unused)]
fn main() {
// Rust
replace_all("/")
}

rpc Action

Use rpc to send the page's variables to the backend in an HTTP POST.

Specify the URL of the backend endpoint.

If the backend returns HTTP 422 Unprocessable Content with a text/plain body, the frontend displays the body to the user in an error screen.

Example POST

Examples

# Ruby
rpc("/login")
#![allow(unused)]
fn main() {
// Rust
rpc("/login")
}

Submitting Data

Coming soon...

Manual Testing

Applin Tester

Applin Tester screenshot

  1. Install Applin Tester on any iOS device
  2. Enter the URL of your backend and tap "Connect"

You can make custom URLs that open Applin Tester to a particular page. The format of the URL is applin:BASE_URL/?p=PAGE1&p=PAGE2.

Use p query parameters to specify stack page keys.

Example: applin:https://rails-demo.applin.dev/?p=/&p=/text_page

If you have Applin Tester installed, this link will open Applin Tester, connect to the Rails Demo, and display the "Text" page, with the home page under it.

Example App

Download an example app and point it at your backend. See iOS Frontend.

Automated Testing

Coming soon...

Releasing

iOS

  1. Create your Team in your Apple Developer account.
  2. Change your app's Team
    • Open the Project Navigator
    • Click on the app name at the top of Project Navigator
    • Click the app name under TARGETS
    • Click the "Signing & Capabilities" tab
    • Change "Team" to your Team identifier.
  3. Edit Main.swift
    • Set licenseKey. Release builds do not start when this is missing or incorrect.
  4. TODO: Add more details.

Support

If you have questions about how to use Applin, please create a ticket at https://github.com/leonhard-llc/applin-support/issues

You can hire me to train your team on Applin and to be on-call to help you fix production problems in your Applin app.

Please email me to start a conversation about your needs: info@leonhardllc.com.

--Michael Leonhard (Applin founder)

Related:

Reporting Bugs

Please create an Issue ticket in the relevant GitHub repository.

Pleae include this information

  • What you expected to happen
  • What actually happened
  • How to reproduce the behavior
  • Any workarounds you found

If someone has already posted about the problem, please post a comment with the above information.

I am eager to make Applin work well for you. Thank you for taking the time to report problems you have with Applin!

-Michael Leonhard (Applin founder)

Feature Requests

Please create a ticket at https://github.com/leonhard-llc/applin-support/issues

Pleae include this information

  • What you are trying to accomplish
  • How the feature will work
  • Alternatives you considered
  • The URL of your app, if any

If someone has already requested the same feature, please post a comment with the above information.

I am eager to make Applin work well for you. Thank you for taking the time to tell me your needs!

-Michael Leonhard (Applin founder)

Appendix

Frontend-Backend Protocol

Applin frontends and backends communicate with JSON and HTTP. The protocol incorporates the core concepts of REST.

Pages and URLs

Every Applin app is accessible at a particular base URL with the form https://HOST/PATH. The URL must not contain query or fragment components.

Every Applin app page has a URL. All page URLs are relative to the base URL. The default "home page" is at relative URL /.

Applin frontends display pages only from the base URL. They do not fetch pages from other servers or URLs. To display content from another server, have your server fetch the content.

For example, if your app's base URL is https://apps.example.com:1234/travel then the page /hotels has URL https://apps.example.com:1234/travel/hotels.

Frontend Role

Every Applin frontend is a program that makes requests to a particular Applin server, receives page specifications from the server, and provides functions for interacting with pages and navigating between pages.

Request types:

  • GET for a page the frontend has not previously requested
    • Request headers
  • GET to refresh a page that has no variables
    • Request headers
      • Accept: application/vnd.applin_response
  • POST to refresh a page that has variables (user-input widgets).
    • Request headers
      • Accept: application/vnd.applin_response
      • Content-Type: application/json request header
    • Request body is a JSON object with the current page's variables.
  • POST for an rpc action on a button.
    • Request headers
      • Content-Type: application/json request header
    • Request body is a JSON object with the current page's variables.
  • GET for page content (images, etc.)

Server Role

Every Applin server is an HTTP server that handles requests.

  • Requests for pages, when request has the Accept: application/vnd.applin_response header
    • Response headers
      • Content-Type: application/vnd.applin_response
      • Response code: 200 OK
      • Response body is a JSON object with the format described below.
      • Do not return 4xx errors for bad user input. Instead, display problems on the page.
  • Form POST (without Accept: application/vnd.applin_response)
    • Response code: 200 OK
    • No response body
    • When the request is missing a required variable, or a variable has the wrong type
      • Response code: 400 Bad Request
      • Response headers: Content-Type: text/plain
      • Response body: technical details about the problem
    • When the user entered data that is unacceptable
      • Response code: 422 Unprocessable Content
      • Response headers: Content-Type: text/plain
      • Response body: a message for the user
  • When the user is not logged in (session cookie missing or invalid) or does not have permission to view the page
    • Response code: 403 Forbidden
    • Response headers: Content-Type: text/plain
    • Response body: a message for the user
  • When the server failed to process the request
    • Response code: 500 Internal Server Error
    • Response headers: Content-Type: text/plain
    • Response body: technical details about the problem
  • When the server is overloaded
    • Response code: 503 Service Unavailable
    • No response body
    • The frontend will retry with backoff.

Applin frontends receive and send cookies like web browsers. Servers can set and receive cookies for session tokens. See Cookies on MDN.

Applin Request Format

The application/json content-type is a JSON object encoded in a UTF-8 string. It contains key-value pairs for all data-entry widgets on the page.

Applin Response Format

The application/vnd.applin_response content-type is a JSON object encoded in a UTF-8 string. It must include these keys:

  • page is an Applin page specification

Example Exchange

Frontend request:

GET /new_account HTTP/1.1

Server response:

HTTP/1.1 200 OK
Content-Type: application/vnd.applin_response
{
  "page": {
    "typ": "nav_page",
    "title": "New Account",
    "widget": {
      "typ": "scroll",
      "widget": {
        "typ": "form",
        "widgets": [
          {
            "typ": "textfield",
            "label": "Username",
            "var_name": "username"
          },
          {
            "typ": "nav_button",
            "text": "Terms",
            "actions": [
              "push:/terms"
            ]
          },
          {
            "typ": "nav_button",
            "text": "Privacy",
            "actions": [
              "push:/privacy"
            ]
          },
          {
            "typ": "checkbox",
            "text": "I agree",
            "var_name": "agree"
          },
          {
            "typ": "form_button",
            "text": "Create Account",
            "actions": [
              "rpc:/create_account",
              "replace_all:/"
            ]
          }
        ]
      }
    }
  }
}

Frontend shows the page:

Mobile phone showing a New Account page

User enters user1 in the text box, checks the "I agree" box, and clicks the "Create Account button".

Frontend performs the first action on the button, "rpc:/create_account", by sending this request:

POST /create_account HTTP/1.1
content-type: application/json
{"agree":true,"username":"user1"}

Server responds with

HTTP/1.1 200 OK

Frontend receives this successful response and then performs the next action on the button, "replace_all:/", removing all visible pages and showing the home page.

Optional - Storyboard Launch Screen

The example app template uses an image for the launch screen. If you want a fancier launch screen, follow these instructions to replace it with a storyboard launch screen.

Note: XCode's storyboard editor is difficult to use. If you just want to add some text to your launch screen, try adding it into your logo.png.

screenshot

  1. Delete the Launch Screen key from Info.plist
  2. Right-click on "Info.plist" in the Project Navigator and choose "New File"
    1. Select iOS and Storyboard and click Next
    2. Name the file LaunchScreen open the storyboard and add the logo:
  3. Click + at the top-right of the screen
  4. Drag Image View onto the empty app screen. Drop it in the center of the screen.
  5. In the "Attributes Inspector" column on the right-hand side, change "Image" to logo.png
  6. Click the unlabeled "Align" button, near the bottom of the window. "Add new alignment constraints" pops up.
    1. Check Horizontally in Container
    2. Check Vertically in Container
    3. Click Add 2 Constraints
  7. Click the unlabeled "Add New Constraints" button, near the bottom of the window. "Add New Constraints" pops up.
    1. Check Width and enter 200 in the box
    2. Check Aspect Ratio
    3. Click Add 2 Constraints
  8. Select View Controller Scene, then click the unlabeled "Show the Attributes Inspector" button in the right-hand pane. Then check the Is Initial View Controller checkbox.
  9. Open target configuration, General tab, and change "Launch Screen File" to LaunchScreen.storyboard.

Optional - Recreate Example App

This section shows how to make a new XCode project and turn it into an Applin frontend.

Note: This is the slow way to make a new app. The fast way is to follow the instructions in iOS Frontend.

1. Create XCode Project

screenshot

  1. Open XCode
  2. "Welcome to Xcode"
    • Click Create a new XCode project
  3. "Choose a template for your new project"
    1. Select iOS
    2. Select App
    3. Click Next
  4. "Choose options for your new project"
    1. Enter your app's name
    2. Select your Apple Developer team account.
    3. Select Storyboard interface
    4. Select Swift language
    5. Click Next
  5. Select the directory to hold the new app directory. Click "Create".

2. Update Files

screenshot

Xcode's left-hand panel is a tab view displaying "navigators". To display the Project Navigator, click the left-most icon or select View > Navigators > Project from the menu.

  1. Delete and Move to Trash each of these items
    • AppDelegate
    • SceneDelegate
    • ViewController
    • Main
    • LaunchScreen
  2. Add logo.png
    1. Save logo.png to your computer
    2. In the Project Navigator, right-click on "Info", and click Add Files to "App Name"... and add the logo.png file you saved.
  3. Add Main.swift
    1. In the Project Navigator, right-click on "Info", and click New File
    2. "Choose a template for your new file" modal appears
      • Select iOS
      • Select Swift File
      • Click Next
      • Type Main into the "Save As" box
      • Click Create
    3. "Would you like to configure an Objective-C bridging header?" modal appears
      • Click Don't Create
    4. The middle panel shows "Main". Replace the contents with https://github.com/leonhard-llc/applin-ios-example/blob/main/Applin%20Example/Main.swift.

3. Update Configuration

  1. Add ApplinIos package
    1. Right-click anywhere in the Project Navigator and click Add Packages
    2. Paste https://github.com/leonhard-llc/applin-ios.git into the search box
    3. The right panel will show "applin-ios" screenshot
    4. Click Add Package
    5. "Choose Package Products for applin-ios" modal appears. 1.Click Add Package
  2. Use logo.png as the launch screen screenshot
    1. In Project Navigator, click on Info
    2. Add the row "Information Property List" > "Launch Screen"
      • Click the + button to add a row
    3. Change the value of row "Information Property List" > "Launch Screen" > "Image Name" to logo.png
  3. Stop UIKit from trying to load "Main" storyboard
    1. In Project Navigator, click on Info
    2. Delete the row "Information Property List" > "Application Scene Manifest" > "Scene Configuration" > "Application Session Role" > "Item 0 (Default Configuration)"
      • Hover over the row and click the - button to delete the row
  4. Allow app to run on iOS 15 (iPhone 6S) and newer screenshot
    1. Open project configuration
      1. In Project Navigator, click on the project, which is top-most item in the Project Navigator It has a blue App Store icon.
      2. Under "PROJECT", click on the app name
    2. Info tab
      1. Change "iOS Deployment Target" to 15.6. This supports iPhone 6S and newer.
  5. Remove unused storyboard settings screenshot
    1. Open target configuration
      1. In Project Navigator, click on the project, which is top-most item in the Project Navigator It has a blue App Store icon.
      2. Under "TARGETS", click on the app name
    2. Info tab
      • Delete these rows:
        • "Main storyboard file base name"
        • "Launch screen interface file base name"