Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 0 additions & 5 deletions Sources/BuilderIO/Components/BuilderImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ struct BuilderImage: BuilderViewProtocol {
(block.component?.options?.dictionaryValue?["fitContent"]?.boolValue ?? false)
&& !(block.children?.isEmpty ?? true)

if let imageBinding = block.codeBindings(for: "image")?.stringValue {
self.imageURL = URL(
string: imageBinding)
}

}

var body: some View {
Expand Down
3 changes: 0 additions & 3 deletions Sources/BuilderIO/Components/BuilderText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ struct BuilderText: BuilderViewProtocol {

self.responsiveStyles = getFinalStyle(responsiveStyles: block.responsiveStyles)

if let textBinding = block.codeBindings(for: "text") {
self.text = textBinding.stringValue
}
}

var body: some View {
Expand Down
3 changes: 0 additions & 3 deletions Sources/BuilderIO/Components/BuilderVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ struct BuilderVideo: BuilderViewProtocol {
if let videoString = options?["video"]?.stringValue {
self.videoURL = URL(string: videoString)
}
if let videoBinding = block.codeBindings(for: "video")?.stringValue {
self.videoURL = URL(string: videoBinding)
}

// Video options
self.autoPlay = options?["autoPlay"]?.boolValue ?? false
Expand Down
256 changes: 181 additions & 75 deletions Sources/BuilderIO/Schemas/BuilderBlockModel.swift
Original file line number Diff line number Diff line change
@@ -1,115 +1,221 @@
import Foundation // For Codable, Data, etc.
import Foundation

// Schema for Builder blocks
// MARK: - Core Data Models

/// Represents a single Builder.io block, conforming to Codable and Identifiable.
public struct BuilderBlockModel: Codable, Identifiable {
public var id: String
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: AnyCodable? = nil
public var code: AnyCodable? = nil
public var meta: AnyCodable? = nil
public var linkUrl: String? = nil
public var `repeat`: [String: String]? = [:]

public var stateBoundObjectModel: StateModel? = nil
public var stateRepeatCollectionKey: StateRepeatCollectionKey? = nil

public var locale: String? = nil // Optional locale for the block
public var properties: [String: String]?
public var bindings: [String: String]?
public var children: [BuilderBlockModel]?
public var component: BuilderBlockComponent?
public var responsiveStyles: BuilderBlockResponsiveStyles
public var actions: AnyCodable?
public var code: AnyCodable?
public var meta: AnyCodable?
public var linkUrl: String?
public var `repeat`: [String: String]?
public var locale: String?

// Internal state properties
var stateBoundObjectModel: StateModel?
var stateRepeatCollectionKey: StateRepeatCollectionKey?

public init(
id: String = UUID().uuidString,
properties: [String: String]? = nil,
bindings: [String: String]? = nil,
children: [BuilderBlockModel]? = nil,
component: BuilderBlockComponent? = nil,
responsiveStyles: BuilderBlockResponsiveStyles = .init(),
actions: AnyCodable? = nil,
code: AnyCodable? = nil,
meta: AnyCodable? = nil,
linkUrl: String? = nil,
`repeat`: [String: String]? = nil,
locale: String? = nil
) {
self.id = id
self.properties = properties
self.bindings = bindings
self.children = children
self.component = component
self.responsiveStyles = responsiveStyles
self.actions = actions
self.code = code
self.meta = meta
self.linkUrl = linkUrl
self.repeat = `repeat`
self.locale = locale
}
}

/// Represents component-specific data for a block.
public struct BuilderBlockComponent: Codable {
public var name: String
public var options: AnyCodable?
}

/// Defines responsive style properties for a block.
public struct BuilderBlockResponsiveStyles: Codable {
public var large: [String: String]?
public var medium: [String: String]?
public var small: [String: String]?

public init(
large: [String: String]? = nil,
medium: [String: String]? = nil,
small: [String: String]? = nil
) {
self.large = large
self.medium = medium
self.small = small
}
}

/// Stores information for repeating a block based on a collection.
public struct StateRepeatCollectionKey: Codable {
public var index: Int
public var collection: String

}

// MARK: - Extension for Logic

extension BuilderBlockModel {
/// Recursively sets the `stateBoundObjectModel` for this block and all its children.
/// Recursively propagates state and applies bindings to this block and its children.
public mutating func propagateStateBoundObjectModel(
_ model: StateModel?, stateRepeatCollectionKey: StateRepeatCollectionKey? = nil
) {
self.stateBoundObjectModel = model
self.stateRepeatCollectionKey = stateRepeatCollectionKey

if var children = self.children {
for index in children.indices {
children[index].propagateStateBoundObjectModel(
model, stateRepeatCollectionKey: stateRepeatCollectionKey)
}
self.children = children
}
applyCodeBindings()
propagateStateToChildren(model, stateRepeatCollectionKey: stateRepeatCollectionKey)
}

public func codeBindings(for key: String) -> AnyCodable? {
guard let code = code,
let codeConfig = code.dictionaryValue,
let bindingsConfig = codeConfig["bindings"],
let bindings = bindingsConfig.dictionaryValue
else {
return nil
/// Sets the locale for the current block and all its children recursively.
public mutating func setLocaleRecursively(_ newLocale: String) {
self.locale = newLocale
self.id = UUID().uuidString // Reset ID for uniqueness after a locale change

guard var children = children else { return }
for index in children.indices {
children[index].setLocaleRecursively(newLocale)
}
self.children = children
}
}

if let stateModel = stateBoundObjectModel {
// MARK: - Private Helpers

extension BuilderBlockModel {
/// Applies code bindings to component options like 'text' and 'image'.
fileprivate mutating func applyCodeBindings() {
var options = component?.options?.dictionaryValue
var styling = responsiveStyles.small ?? [:]

if var options = options {
if let bindingText = evaluateCodeBinding(for: "text") {
options["text"] = bindingText
}

if let bindingImage = evaluateCodeBinding(for: "image") {
options["image"] = bindingImage
}

if let bindingVideo = evaluateCodeBinding(for: "video") {
options["video"] = bindingVideo
}

//binding is in a list
if let stateRepeatCollectionKey = stateRepeatCollectionKey {
let collection = stateModel.getCollectionFromStateData(
keyString: stateRepeatCollectionKey.collection)
self.component?.options = AnyCodable.dictionary(options)
}

let model = collection?[stateRepeatCollectionKey.index].dictionaryValue
if let bindingColor = evaluateCodeBinding(for: "color") {
styling["color"] = bindingColor.stringValue
}

for (bindingKey, value) in bindings {
let lastComponent = bindingKey.split(separator: ".").last.map(String.init)
if key == lastComponent {
if let lookupKey = value.stringValue {
if let backgroundColor = evaluateCodeBinding(for: "backgroundColor") {
styling["backgroundColor"] = backgroundColor.stringValue
}

return model?[lookupKey.split(separator: ".").last.map(String.init) ?? ""]
}
if let borderColor = evaluateCodeBinding(for: "borderColor") {
styling["borderColor"] = borderColor.stringValue
}

}
}
} else {
self.responsiveStyles.small = styling

for (bindingKey, value) in bindings {
let lastComponent = bindingKey.split(separator: ".").last.map(String.init)
if key == lastComponent {
return stateModel.getValueFromStateData(
keyString: value.stringValue ?? "")
}
}

}
}
/// Recursively propagates the state model to child blocks.
fileprivate mutating func propagateStateToChildren(
_ model: StateModel?, stateRepeatCollectionKey: StateRepeatCollectionKey?
) {
guard var children = children else { return }
for index in children.indices {
children[index].propagateStateBoundObjectModel(
model, stateRepeatCollectionKey: stateRepeatCollectionKey)
}
self.children = children
}

/// Evaluates a single code binding for a given key.
fileprivate func evaluateCodeBinding(for key: String) -> AnyCodable? {
guard let bindings = getBindings() else { return nil }

if let stateRepeatCollectionKey = stateRepeatCollectionKey,
let stateModel = stateBoundObjectModel
{
return resolveBindingInRepeatCollection(
key: key, bindings: bindings, stateModel: stateModel,
stateRepeatCollectionKey: stateRepeatCollectionKey)
} else if let stateModel = stateBoundObjectModel {
return resolveBindingInStandardState(key: key, bindings: bindings, stateModel: stateModel)
}

return nil
}

public mutating func setLocaleRecursively(_ newLocale: String) {
self.locale = newLocale
self.id = UUID().uuidString // Reset ID to ensure uniqueness after locale change
if let children = self.children {
var newChildren = children
for i in 0..<newChildren.count {
newChildren[i].setLocaleRecursively(newLocale)
}
self.children = newChildren
}
/// Extracts the bindings dictionary from the code property.
fileprivate func getBindings() -> [String: AnyCodable]? {
return code?.dictionaryValue?["bindings"]?.dictionaryValue
}

}
/// Resolves a binding within a repeated collection.
fileprivate func resolveBindingInRepeatCollection(
key: String, bindings: [String: AnyCodable], stateModel: StateModel,
stateRepeatCollectionKey: StateRepeatCollectionKey
) -> AnyCodable? {
guard
let collection = stateModel.getCollectionFromStateData(
keyString: stateRepeatCollectionKey.collection),
stateRepeatCollectionKey.index < collection.count,
let model = collection[stateRepeatCollectionKey.index].dictionaryValue
else {
return nil
}

public struct BuilderBlockComponent: Codable {
public var name: String
public var options: AnyCodable? = nil // Replaced JSON? with AnyCodable?, default to nil
}
for (bindingKey, value) in bindings {
if bindingKey.split(separator: ".").last.map(String.init) == key,
let lookupKey = value.stringValue,
let finalKey = lookupKey.split(separator: ".").last.map(String.init)
{
return model[finalKey]
}
}
return nil
}

public struct BuilderBlockResponsiveStyles: Codable {
var large: [String: String]? = [:]
var medium: [String: String]? = [:]
var small: [String: String]? = [:]
/// Resolves a binding from the standard state model.
fileprivate func resolveBindingInStandardState(
key: String, bindings: [String: AnyCodable], stateModel: StateModel
) -> AnyCodable? {
for (bindingKey, value) in bindings {
if bindingKey.split(separator: ".").last.map(String.init) == key,
let keyString = value.stringValue
{
return stateModel.getValueFromStateData(keyString: keyString)
}
}
return nil
}
}
Loading