Skip to content
Merged
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
2 changes: 1 addition & 1 deletion 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
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
2 changes: 1 addition & 1 deletion Sources/BuilderIO/Components/BuilderSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftyJSON

struct BuilderSection: BuilderViewProtocol {

var componentType: BuilderComponentType = .section
static let componentType: BuilderComponentType = .section
var children: [BuilderBlockModel]?
var block: BuilderBlockModel
var lazyLoad: Bool = false
Expand Down
2 changes: 1 addition & 1 deletion Sources/BuilderIO/Components/BuilderText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
struct BuilderText: BuilderViewProtocol {
var block: BuilderBlockModel

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

var responsiveStyles: [String: String]?
var text: String?
Expand Down
14 changes: 14 additions & 0 deletions Sources/BuilderIO/ExportedView/BuilderIOPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public struct BuilderIOPage: View {
.refreshable {
await loadPageContent()
}
//When running in Appetize.io, shake event will reload the content
.onReceive(NotificationCenter.default.publisher(for: .deviceDidShakeNotification)) { _ in
if isPreviewing() {
Task {
await loadPageContent()
}
}
}
} else {
Text("No remote content available.")
}
Expand All @@ -50,4 +58,10 @@ public struct BuilderIOPage: View {
print("Already loading content for URL: \(url). Not re-fetching.")
}
}

func isPreviewing() -> Bool {
let isAppetize = UserDefaults.standard.bool(forKey: "isAppetize")
return isAppetize
}

}
6 changes: 3 additions & 3 deletions Sources/BuilderIO/Extensions/ColorExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension Color {
/// - RGBA: "rgba(r, g, b, a)", "rgb(r, g, b)"
/// - Hex: "#RRGGBB", "#RGB", "#RRGGBBAA", "#RGBA"
/// - Named Colors: "red", "blue", "green", "white", "black", "gray", "clear" (and more can be added)
init?(string: String) {
public init?(string: String) {
let cleanedString = string.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()

// MARK: - 1. Try to parse RGBA string
Expand Down Expand Up @@ -113,11 +113,11 @@ extension Color {
}
}

func darker(by percentage: CGFloat = 30) -> Color {
public func darker(by percentage: CGFloat = 30) -> Color {
adjust(brightnessDelta: -percentage / 100.0)
}

func lighter(by percentage: CGFloat = 30) -> Color {
public func lighter(by percentage: CGFloat = 30) -> Color {
adjust(brightnessDelta: percentage / 100.0)
}

Expand Down
14 changes: 14 additions & 0 deletions Sources/BuilderIO/Extensions/UIWindowShakeNotification.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import UIKit

extension UIWindow {
override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
// Post a notification when a shake gesture is detected
NotificationCenter.default.post(name: .deviceDidShakeNotification, object: nil)
}
}
}

extension Notification.Name {
static let deviceDidShakeNotification = Notification.Name("deviceDidShakeNotification")
}
11 changes: 0 additions & 11 deletions Sources/BuilderIO/Helpers/ShakeNotification.swift

This file was deleted.

22 changes: 11 additions & 11 deletions Sources/BuilderIO/Schemas/BuilderBlockModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import SwiftyJSON
// Schema for Builder blocks
public struct BuilderBlockModel: Codable, Identifiable {
public var id: String
var properties: [String: String]? = [:]
var bindings: [String: String]? = [:]
var children: [BuilderBlockModel]? = []
var component: BuilderBlockComponent? = nil
var responsiveStyles: BuilderBlockResponsiveStyles? = BuilderBlockResponsiveStyles() // for inner style of the component
var actions: JSON? = [:]
var code: JSON? = [:]
var meta: JSON? = [:]
var linkUrl: String? = nil
public var properties: [String: String]? = [:]
public var bindings: [String: String]? = [:]
public var children: [BuilderBlockModel]? = []
public var component: BuilderBlockComponent? = nil
public var responsiveStyles: BuilderBlockResponsiveStyles? = BuilderBlockResponsiveStyles() // for inner style of the component
public var actions: JSON? = [:]
public var code: JSON? = [:]
public var meta: JSON? = [:]
public var linkUrl: String? = nil
}

public struct BuilderBlockComponent: Codable {
var name: String
var options: JSON? = [:]
public var name: String
public var options: JSON? = [:]
}

public struct BuilderBlockResponsiveStyles: Codable {
Expand Down
54 changes: 54 additions & 0 deletions Sources/BuilderIO/Services/BuilderContentAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,58 @@ public struct BuilderContentAPI {
return nil
}
}

static func registerCustomComponentInEditor(
component: BuilderCustomComponent, apiKey: String, sessionId: String, sessionToken: String
) async -> Bool {
let overrideUrl = UserDefaults.standard.string(forKey: "builderRemoteUrl")
let urlString = overrideUrl ?? "https://cdn.builder.io/api/v1/remote-sessions/\(sessionId)"

guard var components = URLComponents(string: urlString) else {
print("Error: Bad URL components.")
return false // Indicate failure due to bad URL
}

components.queryItems = [
URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "sessionToken", value: sessionToken),
]

guard let url = components.url else {
print("Error: Could not form URL.")
return false // Indicate failure due to bad URL
}

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

do {
let jsonData = try JSONEncoder().encode(component)
request.httpBody = jsonData
} catch {
print("Error serializing JSON data: \(error)")
return false // Indicate failure due to JSON serialization error
}

do {
let (data, response) = try await URLSession.shared.data(for: request)

if let httpResponse = response as? HTTPURLResponse {
if !(200...299).contains(httpResponse.statusCode) {
print(
"Server error with status code: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "N/A")"
)
return false // Indicate failure due to non-success status code
}
}

print("Successfully registered component. Response data size: \(data.count) bytes")
return true // Indicate success
} catch {
print("Network or unexpected error during registration: \(error)")
return false // Indicate failure for any other network/URLSession error
}
}

}