FlickDocumentation
Swift SDK

Quick start

Integrate Flick with your SwiftUI app.

Add Flick to your app, declare the APIs it can use, and connect them to your SwiftUI hierarchy.

This guide uses an app named MyApp with bundle ID com.acme.myapp. Use your own app name, bundle ID, and trust-root URL when following the examples.

1. Add the package

Flick requires Swift 6.4 or later and supports iOS 18 or later and macOS 15 or later. Swift Package Manager projects must declare Swift tools 5.5 or later in Package.swift:

Package.swift
// swift-tools-version: 5.5

The SDK is in private beta and the repository is private. To request access, email your public SSH deploy key to hi@flick.fyi.

Load the matching private key into your SSH agent before installing the package.

Xcode

Choose File → Add Package Dependencies and enter:

ssh://git@github.com/flickfyi/flick-swift.git

Select the main branch and add the FlickSDK library to your app target.

In the app target’s Build Phases, add FlickSDKPlugin under Run Build Tool Plug-ins.

Swift Package Manager

Add the dependency to your Package.swift:

Package.swift — dependencies
.package(url: "ssh://git@github.com/flickfyi/flick-swift.git", branch: "main"),

Add the library and build plugin to the target containing your Flick integration:

Package.swift — targets
.target(
    name: "MyAppFeature",
    dependencies: [
        .product(name: "FlickSDK", package: "flick-swift"),
    ],
    plugins: [
        .plugin(name: "FlickSDKPlugin", package: "flick-swift"),
    ]
),

Also add FlickSDKPlugin to the app target’s Run Build Tool Plug-ins phase when your integration lives in a Swift package.

The plugin generates Swift accessors and embeds your app’s TypeScript API during the build.

2. Define your app’s integration

The SDK includes flick-sdk, an AST-based generator that reads Swift enums annotated with @FlickScope and produces the TypeScript API used by Flicks.

Declare your app’s integration in MyApp+Flick.swift:

MyApp/MyApp+Flick.swift
import FlickSDK

// Flick discovers the public key for this URL to verify permission
// to publish updates to your app.
@FlickScope(url: "https://acme-corp.com")
// Namespace for your app's actions, state, and views
enum MyApp {
    // Expose Swift operations that Flicks can invoke
    enum Action {
        // @FlickAction(failure: FlickContextError.self)
        // @FlickAction(output: .effect(Int.self))
        // case increment(by: Int)

        // In your SwiftUI view: @State var count = 0
        @FlickAction(failure: Never.self)
        case increment // -> .taskForFlick(for: \.increment) { count += 1 }
    }

    // Expose Swift state for Flick observation
    enum State {
        // In your SwiftUI view: @State var count = 0
        case count(Int) // -> .observeForFlick(count, for: \.count) { Flick.Publish($0) }
    }

    // Mount Flicks as SwiftUI views with inputs from Swift
    enum Views {
        // -> Flick(.card, title: "Hello")
        struct Card: Codable, Sendable {
            let title: String
        }
    }
}

Views, actions, and state are independently optional. Include only the declarations you want to expose. The examples below show each feature separately.

Use your own trust-root URL in @FlickScope. Public-key infrastructure explains how to publish the key for that URL and authorize updates.

3. Mount the SDK

Flick uses your app’s identifier and a callback URL scheme for app-to-app sign-in.

In Xcode, right-click your app’s Info.plist and choose Open As → Source Code. Copy these entries into its top-level <dict>:

Info.plist
<key>AppIdentifierPrefix</key>
<string>$(AppIdentifierPrefix)</string>
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>fyi.flick.sdk.identity-authorization</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER:lower)</string>
        </array>
    </dict>
</array>

If CFBundleURLTypes already exists, add the inner <dict> to its existing array.

Xcode fills in your signing prefix and lowercased bundle ID when it builds.

Create an Info.plist if your project doesn’t have one
  1. Choose File → New → File from Template → Property List, name it Info.plist, and leave all targets unchecked.
  2. In your app target’s Build Settings, set Info.plist File to its project-relative path, such as MyApp/Info.plist. Keep Generate Info.plist File set to Yes so Xcode merges it with your existing settings.

Wrap each scene’s content in FlickSDK. It provides the shared runtime for Flick views, action handlers, state observations, and the Flick control:

MyApp/MyAppApp.swift
import FlickSDK
import SwiftUI

@main
struct MyAppApp: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
            // Uses MyApp's @FlickScope URL as the trust root
            // During development, skip PKI with FlickSDK(trustRoot: nil)
            FlickSDK {
                ContentView()
            }
        }
    }
}

4. Add a Flick view

Place the named view in your SwiftUI hierarchy:

MyApp/ContentView.swift
import FlickSDK
import SwiftUI

struct ContentView: View {
    var body: some View {
        Flick(.card, title: "Hello") // Renders "Hello from Flick" as native SwiftUI Text
    }
}

The plugin generates Flick(.card, title:) from MyApp.Views.Card. The SDK passes title to the TypeScript implementation and renders its native views at this location.

Use flkr to create and publish the implementation:

flicks/src/card.tsx
import { defineView, Text } from '@flickfyi/core'
import { MyApp } from 'com.acme.myapp'

export default defineView(MyApp.views.card, function Card({ title }) {
  return <Text>{title} from Flick</Text>
})

5. Connect a Flick action

Register a handler where your app owns the behavior. Here, the screen owns a counter and handles MyApp.Action.increment by updating that counter:

MyApp/ContentView.swift
import FlickSDK
import SwiftUI

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        Text("Count: \(count)")
            .taskForFlick(for: \.increment) {
                count += 1
            }
    }
}

When a Flick invokes increment, the SDK runs your Swift handler. failure: Never.self declares that this action cannot fail. The handler is available while the view is mounted.

In a Flick opened through the Flick control, get the action with useContextAction and invoke it from an event handler:

Flick.tsx
import { Button, useContextAction } from '@flickfyi/core'
import { Effect } from 'effect'
import { MyApp } from 'com.acme.myapp'

export default function IncrementButton() {
  // Swift: @FlickAction(failure: Never.self) case increment
  // TypeScript: () => Effect.Effect<void, never>
  const increment = useContextAction(MyApp.actions.increment)

  return (
    <Button onClick={() => {
      // Run the Swift handler and return a Promise
      return Effect.runPromise(increment())
    }}>
      Increment
    </Button>
  )
}

Effect.runPromise executes the returned effect.

6. Publish Flick state

Connect MyApp.State.count to a value your app owns with observeForFlick. Here, a native button updates the counter and the SDK publishes its value:

MyApp/ContentView.swift
import FlickSDK
import SwiftUI

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
        .observeForFlick(count, for: \.count) {
            // Publish count to Flicks; this closure can also transform the value
            Flick.Publish($0)
        }
    }
}

The SDK publishes the current value when the view mounts and republishes it when count changes.

Flicks opened through the Flick control can subscribe to the generated state descriptor inside a React component:

Flick.tsx
import { Text, useContextObservation } from '@flickfyi/core'
import { MyApp } from 'com.acme.myapp'

export default function Counter() {
  const count = useContextObservation(MyApp.states.count)

  return <Text>Count: {count}</Text>
}

The hook returns a number and updates the component when the app publishes a new value. To make state writable, also supply a set: closure to observeForFlick:

MyApp/ContentView.swift — writable state alternative
.observeForFlick(count, for: \.count, set: { count = $0 }) {
    Flick.Publish($0)
}

With a setter, the SDK generates a writable state descriptor, which a Flick can access with useContextState.

Flick.tsx
import { Button, Flex, Text, useContextState } from '@flickfyi/core'
import { MyApp } from 'com.acme.myapp'

export default function Counter() {
  const [count, setCount] = useContextState(MyApp.states.count)

  return (
    <Flex>
      <Text>Count: {count}</Text>
      <Button onClick={() => setCount(current => current + 1)}>
        Increment
      </Button>
    </Flex>
  )
}

Clicking Increment calls the Swift set: closure. Swift updates count, and the observation publishes the new value back to the component.

7. Enable the Flick control

Add .flickable(in:) to enable the Flick control on a screen. Its slide menu lets users create and open Flicks in the context you select:

MyApp/MyAppApp.swift — scene excerpt
FlickSDK {
    ContentView()
        .flickable(in: .from(.myApp))
}

.from(.myApp) selects MyApp as the context. Flicks opened here can use the actions and state registered for that namespace.

See Flick control to customize the slide menu and its interaction with your screen.

Generate and inspect the types

Building your app generates the Swift accessors and the TypeScript API that the SDK loads at startup.

To inspect the TypeScript package and write Flick implementations, install flkr, then open it from the directory containing your app’s .xcodeproj:

MyApp/
flkr

flkr calls the SDK generator and writes a TypeScript package to generated/. It also installs a Generate Flick SDK build phase so later Xcode builds refresh that output.

Generated package
generated/
├── manifest.json
├── package.json
└── src/
    ├── index.js
    ├── index.d.ts
    └── FlickContext+Generated.swift

Open generated/src/index.d.ts to see how your Swift integration becomes a TypeScript API. The declarations above produce typed descriptors for the view, action, and read-only state:

generated/src/index.d.ts — excerpt
import type { ContextAction, ContextObservation, ContextView } from "@flickfyi/core"

export const MyApp: {
  readonly "states": {
    readonly "count": ContextObservation<
      "apple:bundle-id:com.acme.myapp/myApp#states/count",
      number,
      never,
      never
    >
  }
  readonly "actions": {
    readonly "increment": ContextAction<
      "apple:bundle-id:com.acme.myapp/myApp#actions/increment",
      void,
      import("effect/Effect").Effect<void, never, never>
    >
  }
  readonly "views": {
    readonly "card": ContextView<
      "apple:bundle-id:com.acme.myapp/myApp#views/card",
      { readonly "title": string }
    >
  }
}

String becomes string, Int becomes number, and the action’s Never failure becomes never. These descriptors type-check a Flick’s use of your app APIs and identify the corresponding Swift view, handler, or state value at runtime.