Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,39 @@ To integrate the Builder Swift SDK into your iOS App:
2. Point to the `main` branch to always receive the latest SDK updates.

3. Import the SDK wherever you need to access its functionality:

```swift
import BuilderIO

```

4. Besure to call BuilderIOManager.configure pass in your APIKey and optional custom navigation scheme

```swift
BuilderIOManager.configure(apiKey: <YOUR_BUILDER_API_KEY>, customNavigationScheme: "builderio")

```

custom navigation scheme <CUSTOM_SCHEME>://<MODEL_NAME>/<PAGE_URL>?<OPTIONAL_PARAMETERS> enables navigation within the native SDK between builder pages.

---

### Render Content

#### Render a Full Page

Use `BuilderIOPage` to render a full page from a given Builder URL:
Use `BuilderIOPage` to render a full page from a given Builder URL and Optional evne handler to process onlick events for components.

```swift
BuilderIOPage(apiKey: "<YOUR_BUILDER_API_KEY>", url: "/YOUR_TARGET_URL")
BuilderIOPage(url: "/YOUR_TARGET_URL", onClickEventHandler: { builderAction in
print("Handle Event Action")
})
```

###### Example:

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

Expand All @@ -51,18 +61,31 @@ You can optionally specify the `model` if you're not using the default `"page"`

#### Render a Section

Use `BuilderIOSection` to render content meant to be embedded in an existing layout:
Use `BuilderIOContentView` to render content (section views) meant to be embedded in an existing layout:
Compuslory to register environment action handler to handle click events

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

##### Example:

```swift
VStack {
BuilderIOSection(apiKey: "<YOUR_BUILDER_API_KEY>", model: "YOUR_MODEL_NAME")
}
@StateObject private var buttonActionManager = BuilderActionManager()

var body: some View {

BuilderIOContentView(model: "hero-section")
.environmentObject(buttonActionManager)
.onAppear {
// Set the action handler
buttonActionManager.setHandler { builderAction in
print("Handle Event Action")
}
}


}
```

---
Expand All @@ -72,8 +95,7 @@ VStack {
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)
BuilderIOPage(url: "/YOUR_TARGET_URL"
.onAppear {
buttonActionManager.setHandler { builderAction in
// Handle your custom action here
Expand All @@ -90,8 +112,7 @@ 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>"
componentView: MyCustomComponent.self
)
```

Expand Down Expand Up @@ -127,7 +148,7 @@ This supports a **live editing workflow** without the need for rebuilding or red
To fetch Builder content manually (e.g., for preview, caching, or custom rendering), use:

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

Expand Down
56 changes: 55 additions & 1 deletion Sources/BuilderIO/BuilderIOManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,53 @@ import SwiftUI

public final class BuilderIOManager {

public static var shared: BuilderIOManager {
guard let instance = _shared else {
fatalError(
"BuilderIOManager has not been configured. Call BuilderIOManager.configure(apiKey:) before accessing shared."
)
}
return instance
}

private static var _shared: BuilderIOManager?

private let apiKey: String
public let customNavigationScheme: String

private static var registered = false

init(apiKey: String) {
public static func configure(apiKey: String, customNavigationScheme: String = "builderio") {
guard _shared == nil else {
print(
"Warning: BuilderIOManager has already been configured. Ignoring subsequent configuration.")
return
}
_shared = BuilderIOManager(apiKey: apiKey, customNavigationScheme: customNavigationScheme)
}

// MARK: - Private Initialization

private init(apiKey: String, customNavigationScheme: String) {
self.apiKey = apiKey
self.customNavigationScheme = customNavigationScheme

if !Self.registered {
BuilderComponentRegistry.shared.initialize()
Self.registered = true
}
}

// MARK: - Public Methods

public func getApiKey() -> String {
return apiKey
}

func getCustomNavigationScheme() -> String {
return customNavigationScheme
}

public func fetchBuilderContent(model: String = "page", url: String? = nil) async -> Result<
BuilderContent, Error
> {
Expand Down Expand Up @@ -47,4 +83,22 @@ public final class BuilderIOManager {
}
URLSession.shared.dataTask(with: url).resume()
}

//Register Custom component
func registerCustomComponentInEditor(_ componentView: any BuilderViewProtocol.Type) {
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
Expand Up @@ -31,33 +31,16 @@ public class BuilderComponentRegistry {
}

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

//Register Custom component
public func registerCustomComponent(
componentView: any BuilderViewProtocol.Type, apiKey: String? = nil
componentView: any BuilderViewProtocol.Type
) {
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)
}
}
if componentView is any BuilderCustomComponentViewProtocol.Type {
BuilderIOManager.shared.registerCustomComponentInEditor(componentView)
}
}

Expand Down
86 changes: 51 additions & 35 deletions Sources/BuilderIO/Components/BuilderBlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import SwiftUI
struct BuilderBlock: View {

var blocks: [BuilderBlockModel]
let alignVertically: Bool
static let componentType: BuilderComponentType = .box

init(blocks: [BuilderBlockModel]) {
init(blocks: [BuilderBlockModel], alignVertically: Bool = true) {
self.blocks = blocks
self.alignVertically = alignVertically
}

var body: some View {
Expand Down Expand Up @@ -37,19 +39,19 @@ struct BuilderBlock: View {
BuilderBlockLayout(
responsiveStyles: responsiveStyles ?? [:], builderAction: builderAction,
component: component
) {
) { alignVerticallyInLayout in // Renamed parameter to avoid confusion
if let component = component {
BuilderComponentRegistry.shared.view(for: child)
} else if let children = child.children, !children.isEmpty {
BuilderBlock(blocks: children)
// Pass the alignVertically from the current block's context
BuilderBlock(blocks: children, alignVertically: alignVerticallyInLayout ?? true)
} else {
Rectangle().fill(Color.clear)
}
}
}

}

}

}
Expand All @@ -59,9 +61,10 @@ struct BuilderBlockLayout<Content: View>: View {
let builderAction: BuilderAction?
let component: BuilderBlockComponent?

@Environment(\.buttonActionManager) private var buttonActionManager
@EnvironmentObject var buttonActionManager: BuilderActionManager

@ViewBuilder let content: () -> Content
// The content closure now takes an optional Bool, which represents the alignment for nested blocks.
@ViewBuilder let content: (_ alignVertically: Bool?) -> Content

var body: some View {

Expand Down Expand Up @@ -104,16 +107,18 @@ struct BuilderBlockLayout<Content: View>: View {
columns: [
GridItem(.adaptive(minimum: 50), spacing: spacing) // Spacing between columns (0 for tight fit like image)
],
spacing: spacing,
content: content
).frame(maxWidth: maxWidth).padding(padding).builderBackground(
responsiveStyles: responsiveStyles
).builderBackground(
responsiveStyles: responsiveStyles
).builderBorder(properties: BorderProperties(responsiveStyles: responsiveStyles))
spacing: spacing
) {
// Call content with the determined alignment for its children
content(false)
}
.frame(maxWidth: maxWidth)
.padding(padding)
.builderBackground(responsiveStyles: responsiveStyles)
.builderBorder(properties: BorderProperties(responsiveStyles: responsiveStyles))
} else if direction == "row" {
let hStackAlignment = CSSAlignments.verticalAlignment(
justify: justify, alignItems: alignItems)
justify: justify, alignItems: alignItems, alignSelf: alignSelf)

let frameAlignment: Alignment =
switch hStackAlignment {
Expand All @@ -124,17 +129,20 @@ struct BuilderBlockLayout<Content: View>: View {
}

HStack(
alignment: hStackAlignment, spacing: spacing
spacing: spacing
) {
content().padding(padding)
.frame(
minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight,
alignment: frameAlignment
).builderBackground(
responsiveStyles: responsiveStyles
).builderBorder(properties: BorderProperties(responsiveStyles: responsiveStyles))
// Call content with the determined alignment for its children
content(true)
.padding(padding)
.if(frameAlignment == .center && component == nil) { view in
view.fixedSize(
horizontal: responsiveStyles["width"] == "100%" ? false : true, vertical: false)
}
.frame(maxWidth: maxWidth, maxHeight: maxHeight, alignment: frameAlignment)
.builderBackground(responsiveStyles: responsiveStyles)
.builderBorder(properties: BorderProperties(responsiveStyles: responsiveStyles))
}
} else {
} else { // Default to VStack (column direction)

let vStackAlignment = CSSAlignments.horizontalAlignment(
marginsLeft: marginLeft, marginsRight: marginRight, justify: justify,
Expand All @@ -150,32 +158,39 @@ struct BuilderBlockLayout<Content: View>: View {
VStack(spacing: 0) {
if marginTop == "auto" { Spacer() }

let componentView: some View = content().padding(padding)
.if(width != nil) { view in
let componentView: some View = content(true) // Call content with the determined alignment
.padding(padding)
.if(width == nil && height == nil) { view in
view.frame(
width: width,
height: height ?? minHeight ?? nil,
minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight,
alignment: (component?.name == BuilderComponentType.text.rawValue)
? (CSSAlignments.textAlignment(responsiveStyles: responsiveStyles)).toAlignment
: .center
).builderBackground(responsiveStyles: responsiveStyles).builderBorder(
)
.builderBackground(responsiveStyles: responsiveStyles)
.builderBorder(
properties: BorderProperties(responsiveStyles: responsiveStyles)
)
}
.if(width == nil) { view in
.if(width == nil && height != nil) { view in
view.frame(maxWidth: .infinity)
}
.if(width != nil || height != nil) { view in
view.frame(
minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight,
width: width,
height: height ?? minHeight ?? nil,
alignment: (component?.name == BuilderComponentType.text.rawValue)
? (CSSAlignments.textAlignment(responsiveStyles: responsiveStyles)).toAlignment
: .center
).builderBackground(responsiveStyles: responsiveStyles).builderBorder(
properties: BorderProperties(responsiveStyles: responsiveStyles)
)
).builderBackground(responsiveStyles: responsiveStyles)
.builderBorder(
properties: BorderProperties(responsiveStyles: responsiveStyles)
)
}

if let builderAction = builderAction {
Button {
buttonActionManager?.handleButtonPress(builderAction: builderAction)
buttonActionManager.handleButtonPress(builderAction: builderAction)
} label: {
componentView
}
Expand All @@ -184,7 +199,8 @@ struct BuilderBlockLayout<Content: View>: View {
}

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