Skip to content
Merged
172 changes: 122 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,167 @@
# BuilderIO-Swift

The SDK for BuilderIO for iOS in Swift.
The official Swift SDK to render Builder.io content in your iOS app using SwiftUI.

## Using this SDK

### Add dependency
### iOS Compatibility

* Add a dependency on the Builder Swift SDK in your iOS App via the Github package: https://github.com/BuilderIO/builder-swift
* Point to the `main` branch of the repository to get the latest and greatest SDK code
* Import `BuilderIO` wherever you need to use the SDK methods
This SDK supports **iOS 17 and above** to take advantage of the latest SwiftUI capabilities and improved layout behaviors.

### (Optional) Register your custom components

Register any components you have created in your iOS App using something like
### Add Dependency

```
###TODO
```
To integrate the Builder Swift SDK into your iOS App:

## Render Content
1. Add a dependency using the GitHub repository:
[https://github.com/BuilderIO/builder-swift](https://github.com/BuilderIO/builder-swift)

At the location where you want to render the content fetched from Builder
2. Point to the `main` branch to always receive the latest SDK updates.

For a standalone page instantiate
3. Import the SDK wherever you need to access its functionality:

```swift
import BuilderIO
```
---

### Render Content

#### Render a Full Page

Use `BuilderIOPage` to render a full page from a given Builder URL:

```swift
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL")
```
In the above, model is optional set to `page` (add if the model you used is not the default page model in Builder)

eg
```
###### Example:

```swift
var body: some View {
NavigationStack {
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL")
}
NavigationStack {
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL")
}
}
```

For a section instantiate
```
BuilderIOSection(apiKey: "<YOUR_BUILDER_API_KEY>", model: "YOUR_MODEL_NAME")
```
eg
You can optionally specify the `model` if you're not using the default `"page"` model.

---

#### Render a Section

Use `BuilderIOSection` to render content meant to be embedded in an existing layout:

```swift
BuilderIOSection(apiKey: "<YOUR_BUILDER_API_KEY>", model: "YOUR_MODEL_NAME")
```

##### Example:

```swift
VStack {
BuilderIOSection(apiKey: "<YOUR_BUILDER_API_KEY>", model: "YOUR_MODEL_NAME")
}
BuilderIOSection(apiKey: "<YOUR_BUILDER_API_KEY>", model: "YOUR_MODEL_NAME")
}
```

Alternatively, if you want to override the click handling and want to intercept each `Button` click, then you register the environment buttonActionManager for open links and setHandler for custom actions
---

```
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL").environment(\.buttonActionManager, buttonActionManager)
.onAppear {
buttonActionManager.setHandler { builderAction in
print("Handle Event Action")
}
}
### Custom Click Handling

To intercept and handle clicks (e.g., for `button` components), you can override the default behavior using `buttonActionManager`:

```swift
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL")
.environment(\.buttonActionManager, buttonActionManager)
.onAppear {
buttonActionManager.setHandler { builderAction in
// Handle your custom action here
print("Custom Action Triggered: \(builderAction)")
}
}
```

---

## Fetch Content
### (Optional) Register Custom Components

Based on needs request to raw model data can be gathered by instantiating BuilderIOManager with your API key and requesting the your required model /URL
You can register your own custom SwiftUI views to be rendered by Builder using:

```swift
BuilderComponentRegistry.shared.registerCustomComponent(
componentView: MyCustomComponent.self,
apiKey: "<YOUR_BUILDER_API_KEY>"
)
```
BuilderIOManager(apiKey:"<YOUR_BUILDER_API_KEY>").fetchBuilderContent(model: String = "YOUR_MODEL_NAME", url: String? = "/YOUR_TARGET_URL")

> Replace `MyCustomComponent` with the name of your custom SwiftUI view.

Custom components **must conform** to the `BuilderCustomComponentViewProtocol`.

---

To enable live editing and previewing of your custom components inside the [Builder.io Visual Editor](https://www.builder.io/):

1. Upload a **simulator build** of your app to [Appetize.io](https://appetize.io).
2. Link your **Appetize build ID** in your Builder.io space under **Connected Devices**.
3. Once your Builder.io page loads inside the Appetize-hosted simulator, the component registration will be completed automatically.

This setup enables **real-time editing** and **custom component preview** within Builder’s visual editor.

---

#### Handle Preview Updates from the WebApp

To handle live preview updates:

- Ensure your app is uploaded to **Appetize.io** and **linked** to your Builder.io space.
- Builder.io pushes content updates to the app running in Appetize, allowing you to see changes immediately as you edit.

This supports a **live editing workflow** without the need for rebuilding or redeploying your app for every update.

---

### Fetch Content (Raw Data)

To fetch Builder content manually (e.g., for preview, caching, or custom rendering), use:

```swift
BuilderIOManager(apiKey: "<YOUR_BUILDER_API_KEY>")
.fetchBuilderContent(model: "YOUR_MODEL_NAME", url: "/YOUR_TARGET_URL")
```

This returns the raw JSON or decoded model data from Builder.

---


## Complete Example

https://github.com/aarondemelo/BuilderIOExample

---

## Handle Preview Updates from the WebApp
## Current Support

###TODO
| Builder Component | Color | Margin / Padding | Horizontal Alignment | Click Support | Unsupported Features |
|-----------------------|:-----:|:-----------------:|:----------------------:|:--------------:|
| **Button** | ✅ | ✅ | ✅ | ✅ |
| **Text** | ✅ | ✅ | ✅ | ✅ |
| **Image** | ✅ | ✅ | ✅ | ✅ | Image Position, Lock Aspect Ratio
| **Columns** | ✅ | ✅ | ✅ | ✅ |
| **Sections** | ✅ | ✅ | ✅ | ✅ | Lazy Load
| **Custom** | ✅ | ✅ | ✅ | ✅ |
| **Video** | 🏗 | 🏗 | 🏗 | 🏗 |

## Complete example

For a complete example of an iOS app using the Builder SDK, please refer to ###TODO
You can use this as a base app, and replace the API key in the example with your own to try it out.
**Unsupported**
JS Code Execution, Data Binding, API Data

## Current Support:
---

#### Unsupported in Layouts
- Grid Layout with variable sized components

| Builder Component|Color|Margin / Padding|Horizontal Alignment|Click Support|
|------------------|-----|----------------|--------------------|-------------|
| Button | ✅ | ✅ | ✅ | ✅ |
| Text | ✅ | ✅ | ✅ | ✅ |
| Image | ✅ | ✅ | ✅ | ✅ |
| Columns | ✅ | ✅ | ✅ | ✅ |
| Video | 🏗 | 🏗 | 🏗 | 🏗 |

## Contributing
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import SwiftUI

protocol BuilderViewProtocol: View {
var componentType: BuilderComponentType { get }
public protocol BuilderViewProtocol: View {
static var componentType: BuilderComponentType { get }
var block: BuilderBlockModel { get }
init(block: BuilderBlockModel)
}

public protocol BuilderCustomComponentViewProtocol: BuilderViewProtocol {
static var builderCustomComponent: BuilderCustomComponent { get }
}

extension BuilderViewProtocol {
func getFinalStyle(responsiveStyles: BuilderBlockResponsiveStyles?) -> [String: String] {
return CSSStyleUtil.getFinalStyle(responsiveStyles: responsiveStyles)
}
}

struct BuilderEmptyView: BuilderViewProtocol {
var block: BuilderBlockModel
static let componentType: BuilderComponentType = .empty

var componentType: BuilderComponentType = .empty
var block: BuilderBlockModel

init(block: BuilderBlockModel) {
self.block = block
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SwiftUI

//BuilderComponentRegistry single instance factory for building preregistered components
class BuilderComponentRegistry {
static let shared = BuilderComponentRegistry()
public class BuilderComponentRegistry {
public static let shared = BuilderComponentRegistry()

//Component Registry
private var registry: [BuilderComponentType: any BuilderViewProtocol.Type] = [:]
Expand All @@ -23,16 +23,42 @@ class BuilderComponentRegistry {

//Register default components
func initialize() {
register(type: .text, viewClass: BuilderText.self)
register(type: .image, viewClass: BuilderImage.self)
register(type: .coreButton, viewClass: BuilderButton.self)
register(type: .columns, viewClass: BuilderColumns.self)
register(type: .section, viewClass: BuilderSection.self)
register(type: BuilderText.componentType, viewClass: BuilderText.self)
register(type: BuilderImage.componentType, viewClass: BuilderImage.self)
register(type: BuilderButton.componentType, viewClass: BuilderButton.self)
register(type: BuilderColumns.componentType, viewClass: BuilderColumns.self)
register(type: BuilderSection.componentType, viewClass: BuilderSection.self)
}

private func register(type: BuilderComponentType, viewClass: any BuilderViewProtocol.Type) {
if registry[type] == nil {
registry[type] = viewClass
}
}

//Register Custom component
func register(type: BuilderComponentType, viewClass: any BuilderViewProtocol.Type) {
registry[type] = viewClass
public func registerCustomComponent(
componentView: any BuilderViewProtocol.Type, apiKey: String? = nil
) {
registry[componentView.componentType] = componentView

if componentView is any BuilderCustomComponentViewProtocol.Type, let apiKey = apiKey {

let sessionId = UserDefaults.standard.string(forKey: "builderSessionId")
let sessionToken = UserDefaults.standard.string(forKey: "builderSessionToken")

if let sessionId = sessionId, let sessionToken = sessionToken {

let componentDTO = (componentView as! any BuilderCustomComponentViewProtocol.Type)
.builderCustomComponent

Task {
await BuilderContentAPI.registerCustomComponentInEditor(
component: componentDTO, apiKey: apiKey, sessionId: sessionId,
sessionToken: sessionToken)
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
struct BuilderComponentType: Equatable, Hashable {
public struct BuilderComponentType: Equatable, Hashable {
let rawValue: String

static let text = BuilderComponentType(rawValue: "Text")
Expand All @@ -10,7 +10,7 @@ struct BuilderComponentType: Equatable, Hashable {
static let empty = BuilderComponentType(rawValue: "Empty")

// Add new types dynamically
static func custom(_ name: String) -> BuilderComponentType {
public static func custom(_ name: String) -> BuilderComponentType {
return BuilderComponentType(rawValue: name)
}

Expand Down
11 changes: 8 additions & 3 deletions Sources/BuilderIO/Components/BuilderBlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SwiftUI
struct BuilderBlock: View {

var blocks: [BuilderBlockModel]
var componentType: BuilderComponentType = .box
static let componentType: BuilderComponentType = .box
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have these been made static?

Copy link
Contributor Author

@aarondemelo aarondemelo Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static to make it visible externally during registration without the need to repeat the type instantiation.
eg. for the internal component registration workflow

  • register(type: BuilderImage.componentType, viewClass: BuilderImage.self)

It enables easier custom component registration

  • BuilderComponentRegistry.shared.registerCustomComponent(
    componentView: RatingsComponent.self, apiKey: "###############")

which internally performs registry[componentView.componentType] = componentView


init(blocks: [BuilderBlockModel]) {
self.blocks = blocks
Expand Down Expand Up @@ -43,7 +43,7 @@ struct BuilderBlock: View {
} else if let children = child.children, !children.isEmpty {
BuilderBlock(blocks: children)
} else {
EmptyView()
Rectangle().fill(Color.clear)
}
}
}
Expand Down Expand Up @@ -87,6 +87,7 @@ struct BuilderBlockLayout<Content: View>: View {
let minHeight = extractPixels(responsiveStyles["minHeight"])
let maxHeight = extractPixels(responsiveStyles["maxHeight"])
let width = extractPixels(responsiveStyles["width"])
let height = extractPixels(responsiveStyles["height"])

let minWidth = extractPixels(responsiveStyles["minWidth"])
let maxWidth =
Expand Down Expand Up @@ -153,6 +154,7 @@ struct BuilderBlockLayout<Content: View>: View {
.if(width != nil) { view in
view.frame(
width: width,
height: height ?? minHeight ?? nil,
alignment: (component?.name == BuilderComponentType.text.rawValue)
? (CSSAlignments.textAlignment(responsiveStyles: responsiveStyles)).toAlignment
: .center
Expand Down Expand Up @@ -182,7 +184,10 @@ struct BuilderBlockLayout<Content: View>: View {
}

if marginBottom == "auto" { Spacer() }
}.frame(maxWidth: frameAlignment == .center ? nil : .infinity, alignment: frameAlignment)
}.if(frameAlignment == .center && component == nil) { view in
view.fixedSize(horizontal: true, vertical: false)
}
.frame(maxWidth: frameAlignment == .center ? nil : .infinity, alignment: frameAlignment)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/BuilderIO/Components/BuilderButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftUI
//Wrapped Text Click event handle externally at the layout level
struct BuilderButton: BuilderViewProtocol {

var componentType: BuilderComponentType = .coreButton
static let componentType: BuilderComponentType = .coreButton

var block: BuilderBlockModel

Expand Down
2 changes: 1 addition & 1 deletion Sources/BuilderIO/Components/BuilderColumns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftUI
import SwiftyJSON

struct BuilderColumns: BuilderViewProtocol {
var componentType: BuilderComponentType = .columns
static let componentType: BuilderComponentType = .columns
var block: BuilderBlockModel

var columns: [BuilderContentData]
Expand Down
2 changes: 1 addition & 1 deletion Sources/BuilderIO/Components/BuilderImage.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

struct BuilderImage: BuilderViewProtocol {
var componentType: BuilderComponentType = .image
static let componentType: BuilderComponentType = .image

var block: BuilderBlockModel
var children: [BuilderBlockModel]?
Expand Down
Loading