Skip to content
2 changes: 1 addition & 1 deletion Sources/BuilderIO/BuilderIOManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public final class BuilderIOManager {
model: model,
apiKey: apiKey,
url: resolvedUrl,
locale: "",
locale: nil, //Always send to nil so as to get all options to allow for local changes without requiring an API call

Choose a reason for hiding this comment

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

its ok to make another API call. User properties are part of targeting and the content api may not return the correct content if locale is not passed and the targeting attributes are not accurate. locale also really shouldnt change at any point after the app is launched (it may, but if the screen isnt immediately reactive to it thats fine)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Post discussions with @sanyamkamat aligned implementations with React.

Skipping locale in the Request was an additional step from me to reduce network calls in the sdk.
I will revert the change and pass locale if set by the user or Default. locale: locale ?? "Default"

Every subsequent change in locale will trigger a request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

locale is now passed in the content api as well as if required in the data binding http requests.

preview: ""
) {
return .success(content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ extension BuilderViewProtocol {
func codeBindings() -> [String: String]? {
return nil
}

func localize(localizedValue: AnyCodable) -> String? {

if let localeDictionary = localizedValue.dictionaryValue {
if let currentLocale = block.locale {
if let localizedString = localeDictionary[currentLocale]?.stringValue {
return localizedString
}
}

return localeDictionary["Default"]?.stringValue

} else {
return localizedValue.stringValue
}
}
}

struct BuilderEmptyView: BuilderViewProtocol {
Expand Down
10 changes: 10 additions & 0 deletions Sources/BuilderIO/Components/BuilderColumns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ struct BuilderColumns: BuilderViewProtocol {
}
}

if let locale = block.locale {

for columnIndex in decodedColumns.indices {
for blockIndex in decodedColumns[columnIndex].blocks.indices {
decodedColumns[columnIndex].blocks[blockIndex]
.setLocaleRecursively(locale)
}
}
}

self.columns = decodedColumns

} else {
Expand Down
10 changes: 7 additions & 3 deletions Sources/BuilderIO/Components/BuilderImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct BuilderImage: BuilderViewProtocol {
var block: BuilderBlockModel
var children: [BuilderBlockModel]?

var imageURL: URL?
var imageURL: URL? = nil
var aspectRatio: CGFloat? = nil
var lockAspectRatio: Bool = false
var contentMode: ContentMode = .fit
Expand All @@ -16,8 +16,12 @@ struct BuilderImage: BuilderViewProtocol {

init(block: BuilderBlockModel) {
self.block = block
self.imageURL = URL(
string: block.component?.options?.dictionaryValue?["image"]?.stringValue ?? "")

if let imageLink = block.component?.options?.dictionaryValue?["image"] {
self.imageURL = URL(
string: localize(localizedValue: imageLink) ?? "")
}

if let ratio = block.component?.options?.dictionaryValue?["aspectRatio"]?.doubleValue {
self.aspectRatio = CGFloat(1 / ratio)
}
Expand Down
98 changes: 23 additions & 75 deletions Sources/BuilderIO/Components/BuilderText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ struct BuilderText: BuilderViewProtocol {

init(block: BuilderBlockModel) {
self.block = block
self.text = block.component?.options?.dictionaryValue?["text"]?.stringValue ?? ""
var processedText: String = ""
if let textValue = block.component?.options?.dictionaryValue?["text"] {
self.text = localize(localizedValue: textValue) ?? ""
}

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

if let textBinding = block.codeBindings(for: "text") {
Expand Down Expand Up @@ -106,12 +110,25 @@ struct HTMLTextView: View {
// Perform the NSAttributedString conversion on the MainActor
await MainActor.run {
do {
let nsAttributedString = try NSAttributedString(
data: data,
options: [

var attributedOptions: [NSAttributedString.DocumentReadingOptionKey: Any] = [:]

if #available(iOS 18.0, *) {
attributedOptions = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue,
],
.textKit1ListMarkerFormatDocumentOption: true,
]
} else {
attributedOptions = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue,
]
}

let nsAttributedString = try NSAttributedString(
data: data,
options: attributedOptions,
documentAttributes: nil
)

Expand All @@ -121,6 +138,7 @@ struct HTMLTextView: View {
else {
throw HTMLProcessingError.attributedStringConversionFailed
}

self.attributedString = swiftUIAttributedString
self.errorInProcessing = nil // Clear error if successful
} catch {
Expand Down Expand Up @@ -227,73 +245,3 @@ struct HTMLTextView: View {
}

}

struct BuilderText_Previews: PreviewProvider {
static let builderJSONString = """
{
"@type": "@builder.io/sdk:Element",
"@version": 2,
"id": "builder-54d67576377d4a9293c6f8d2efcda0ef",
"meta": {
"previousId": "builder-ad756879bc6c4ee3ac7977d5af0b6811"
},
"component": {
"name": "Text",
"options": {
"text": "<h1><strong>Right<em> </em></strong><em>Align</em><strong><em> </em></strong><strong style=\\\"color: rgb(144, 19, 254);\\\"><em><u>Text</u></em></strong></h1><p> This is a paragraph with some content that will determine the height dynamically. This text should wrap to multiple lines if the width is constrained.</p>"
},
"isRSC": null
},
"responsiveStyles": {
"large": {
"display": "flex",
"flexDirection": "column",
"position": "relative",
"flexShrink": "0",
"boxSizing": "border-box",
"marginTop": "20px",
"lineHeight": "normal",
"height": "auto",
"marginLeft": "auto",
"paddingLeft": "20px",
"marginRight": "20px"
},
"medium": {
"display": "none"
},
"small": {
"borderWidth": "2px",
"borderStyle": "solid",
"borderColor": "rgba(219, 20, 20, 1)",
"backgroundColor": "rgba(80, 227, 194, 1)",
"backgroundRepeat": "no-repeat",
"backgroundPosition": "center",
"backgroundSize": "cover",
"display": "flex",
"fontSize": "12px",
"fontWeight": "600",
"fontFamily": "Aldrich, sans-serif"
}
}
}
"""

static func decodeBuilderBlockModel(from jsonString: String) -> BuilderBlockModel? {
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
return try decoder.decode(BuilderBlockModel.self, from: data)
} catch {
print("Decoding failed:", error)
return nil
}
}

static var previews: some View {
if let block = decodeBuilderBlockModel(from: builderJSONString) {
BuilderText(block: block)
} else {
Text("Failed to decode block")
}
}
}
21 changes: 17 additions & 4 deletions Sources/BuilderIO/ExportedView/BuilderIOContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ public struct BuilderIOContentView: View {
let url: String?

@State private var viewModel: BuilderIOViewModel
@Binding var locale: String

public init(model: String) {
public init(model: String, locale: String) {
self.init(model: model, locale: .constant(locale))
}

public init(model: String, locale: Binding<String>) {
self.model = model
self.url = nil
_viewModel = State(wrappedValue: BuilderIOViewModel())
self._locale = locale // Initialize the binding
_viewModel = State(wrappedValue: BuilderIOViewModel(locale: locale.wrappedValue))
}

public init(url: String, model: String = "page", locale: String) {
self.init(url: url, model: model, locale: .constant(locale))
}

init(url: String, model: String = "page") {
init(url: String, model: String = "page", locale: Binding<String>) {
self.url = url
self.model = model
_viewModel = State(wrappedValue: BuilderIOViewModel())
self._locale = locale
_viewModel = State(wrappedValue: BuilderIOViewModel(locale: locale.wrappedValue))
}

public var body: some View {
Expand Down Expand Up @@ -66,6 +77,8 @@ public struct BuilderIOContentView: View {
if viewModel.builderContent == nil && !viewModel.isLoading {
await loadContent()
}
}.onChange(of: locale) {
viewModel.updateLocale(locale: locale)
}
}

Expand Down
17 changes: 14 additions & 3 deletions Sources/BuilderIO/ExportedView/BuilderIOPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,36 @@ public struct BuilderIOPage: View {

let url: String
let model: String
@Binding var locale: String

@StateObject private var buttonActionManager = BuilderActionManager()
var onClickEventHandler: ((BuilderAction) -> Void)? = nil

@State private var activeNavigationTarget: NavigationTarget? = nil

public init(
url: String, model: String = "page", onClickEventHandler: ((BuilderAction) -> Void)? = nil
url: String, model: String = "page", locale: String = "Default",
onClickEventHandler: ((BuilderAction) -> Void)? = nil
) {
self.init(
url: url, model: model, locale: .constant(locale), onClickEventHandler: onClickEventHandler)
}

public init(
url: String, model: String = "page", locale: Binding<String>,
onClickEventHandler: ((BuilderAction) -> Void)? = nil
) {
self.url = url
self.model = model
self.onClickEventHandler = onClickEventHandler
self._locale = locale
}

public var body: some View {
NavigationStack(path: $buttonActionManager.path) {
BuilderIOContentView(url: url, model: model)
BuilderIOContentView(url: url, model: model, locale: $locale)
.navigationDestination(for: NavigationTarget.self) { target in
BuilderIOContentView(url: target.url, model: target.model)
BuilderIOContentView(url: target.url, model: target.model, locale: $locale)
}
}.environmentObject(buttonActionManager).onAppear {
if let onClickEventHandler = onClickEventHandler {
Expand Down
24 changes: 23 additions & 1 deletion Sources/BuilderIO/ExportedView/BuilderIOViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,26 @@ public final class BuilderIOViewModel {
public var isLoading: Bool = false
public var errorMessage: String?
public var stateModel: StateModel = StateModel()
public var locale: String = "Default"

public var isNetworkAvailable: Bool = false
private let networkMonitor = NWPathMonitor()
private let networkQueue = DispatchQueue(label: "NetworkMonitorQueue")

/// Initializes the BuilderIOViewModel.
public init() {
public init(locale: String) {
self.locale = locale
startNetworkMonitoring()
}

public func updateLocale(locale: String) {
self.locale = locale
for i in 0..<(self.builderContent?.data.blocks.count ?? 0) {
self.builderContent?.data.blocks[i].setLocaleRecursively(locale)
}

}

private func startNetworkMonitoring() {
networkMonitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor in // Ensure UI updates are on the main actor
Expand Down Expand Up @@ -67,7 +77,14 @@ public final class BuilderIOViewModel {
}

if self.stateModel.apiResponses.isEmpty {
var newContentBlocks = fetchedContent.data.blocks ?? []
for i in 0..<newContentBlocks.count {
newContentBlocks[i].setLocaleRecursively(locale)
}

self.builderContent = fetchedContent

self.builderContent?.data.blocks = newContentBlocks
} else {
// Further logic for content binding/loops can go here if needed.
var contentBlocks = fetchedContent.data.blocks ?? []
Expand Down Expand Up @@ -103,8 +120,13 @@ public final class BuilderIOViewModel {
}

}

self.builderContent = fetchedContent

for i in 0..<newContentBlocks.count {
newContentBlocks[i].setLocaleRecursively(locale)
}

self.builderContent?.data.blocks = newContentBlocks
}

Expand Down
14 changes: 14 additions & 0 deletions Sources/BuilderIO/Schemas/BuilderBlockModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public struct BuilderBlockModel: Codable, Identifiable {
public var stateBoundObjectModel: StateModel? = nil
public var stateRepeatCollectionKey: StateRepeatCollectionKey? = nil

public var locale: String? = nil // Optional locale for the block

}

public struct StateRepeatCollectionKey: Codable {
Expand Down Expand Up @@ -87,6 +89,18 @@ extension BuilderBlockModel {
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
}
}

}

public struct BuilderBlockComponent: Codable {
Expand Down
Loading