Requirements: iOS 17+, SwiftUI, Swift 6. The package has no third-party dependencies.
1. Install
In Xcode, File → Add Package Dependencies… and enter
https://github.com/tweaklane/tweaklane-ios, or in Package.swift:
.package(url: "https://github.com/tweaklane/tweaklane-ios", from: "1.0.0")
Add the TweakLane product to your app target.
2. Register your components
import TweakLane
@MainActor
func makeCatalog() -> ComponentCatalog {
var catalog = ComponentCatalog()
let titled = PropsSchema(properties: ["title": .string], required: ["title"])
catalog.register("stats.list", displayName: "List", alternatives: ["stats.chart"],
props: titled, defaultProps: ["title": "This week"], baseHeight: 160) { props, context in
StepsList(title: props.string("title"))
}
catalog.register("stats.chart", displayName: "Chart", props: titled,
defaultProps: ["title": "This week"], baseHeight: 160) { props, _ in
StepsChart(title: props.string("title"))
}
catalog.register("account.delete", displayName: "Delete account", lock: .required) { _, _ in
DeleteAccountRow()
}
return catalog
}
See Catalog for every parameter and Locks for .required and
.fixed.
3. Provide a default document per screen
let today = LayoutDocument(screen: "today", root: "root", nodes: [
.container(.scroll, id: "root", parent: nil, padding: 16),
.component("greeting", id: "greeting", parent: "root", order: 0, props: ["name": "Sam"]),
.container(.vstack, id: "summary", parent: "root", order: 1, label: "Summary"),
.component("stats.list", id: "stats", parent: "summary", order: 0, props: ["title": "Steps"]),
.component("account.delete", id: "account", parent: "root", order: 3)
])
The default document is the fallback for every failure, so it must be valid. In debug builds
registerDefault asserts on it.
4. Create the engine and render screens
@main
struct MyApp: App {
@State private var model = AppModel()
@State private var tweak: TweakLaneEngine = {
let engine = TweakLaneEngine(catalog: makeCatalog(), userId: "local")
engine.registerDefault(today)
return engine
}()
var body: some Scene {
WindowGroup {
TodayView().environment(model).environment(tweak)
}
}
}
struct TodayView: View {
var body: some View { TweakScreen(id: "today") }
}
TweakScreen reads the saved layout once per session, validates it and renders it. The layout
only changes while the user edits; TweakLane never swaps the UI under the user.
5. Open edit mode
Button("Customize") { tweak.beginEditing("today") }
Each part shows a compact toolbar (drag handle, name, lock badge, Hide and an edit menu). The
tray at the bottom holds hidden parts, Add, Reset and Done. When your screen wraps the
TweakScreen in its own ScrollView, pin the tray outside it with .tweakEditTray("today").
Keep important state in your own @Observable model, not in a component's @State: moving or
swapping a component gives it a new SwiftUI identity, which resets its @State.
6. Turn on sync
Create an app in the dashboard to get its public SDK key and API URL, then:
let engine = TweakLaneEngine(
catalog: makeCatalog(),
userId: account.id,
sync: SyncConfiguration(
baseURL: URL(string: "{TWEAKLANE_URL}")!,
publishableKey: "tl_pk_…",
tokenProvider: ClosureTokenProvider { userId, forceRefresh in
try await myBackend.tweakLaneToken(for: userId, refresh: forceRefresh)
}))
Your backend issues the user token. See Connect your backend. Call
engine.setUser(_:) when the signed-in user changes.
Privacy manifest
The package ships PrivacyInfo.xcprivacy: no tracking, no tracking domains, no required-reason
APIs. With sync configured it declares two collected data types, both linked to the user, not
used for tracking, purpose App Functionality: User ID and Other User Content. Without
sync nothing leaves the device.
Next: Connect your backend.