diff --git a/Sources/BuilderIO/Components/BuilderBlock.swift b/Sources/BuilderIO/Components/BuilderBlock.swift new file mode 100644 index 0000000..915903f --- /dev/null +++ b/Sources/BuilderIO/Components/BuilderBlock.swift @@ -0,0 +1,158 @@ +import SwiftUI +import SwiftyJSON + +//BuilderBlock forms the out layout container for all components mimicking Blocks from response. As blocks can have layout direction of either horizontal or vertical a check is made and layout selected. + +struct BuilderBlock: View { + + var blocks: [BuilderBlockModel] + var componentType: BuilderComponentType = .box + + init(blocks: [BuilderBlockModel]) { + self.blocks = blocks + } + + var body: some View { + + ForEach(Array(blocks.enumerated()), id: \.offset) { index, child in + let responsiveStyles = CSSStyleUtil.getFinalStyle(responsiveStyles: child.responsiveStyles) + + BuilderBlockLayout(responsiveStyles: responsiveStyles ?? [:]) { + if let component = child.component { + BuilderComponentRegistry.shared.view(for: child) + } else if let children = child.children, !children.isEmpty { + BuilderBlock(blocks: children) + } else { + Spacer() + } + } + + } + + } + +} + +struct BuilderBlockLayout: View { + let responsiveStyles: [String: String] + @ViewBuilder let content: () -> Content + + var body: some View { + + // 1. Extract basic layout parameters + let direction = responsiveStyles["flexDirection"] ?? "column" + let wrap = responsiveStyles["flexWrap"] == "wrap" + let scroll = responsiveStyles["overflow"] == "auto" && direction == "row" + + let justify = responsiveStyles["justifyContent"] + let alignItems = responsiveStyles["alignItems"] + + let marginLeft = responsiveStyles["marginLeft"]?.lowercased() + let marginRight = responsiveStyles["marginRight"]?.lowercased() + + let spacing = extractPixels(responsiveStyles["gap"]) ?? 0 + let padding = extractEdgeInsets(for: "padding", from: responsiveStyles) + let margin = extractEdgeInsets(for: "margin", from: responsiveStyles) + + let minHeight = extractPixels(responsiveStyles["minHeight"]) + let maxHeight = extractPixels(responsiveStyles["maxHeight"]) + let minWidth = extractPixels(responsiveStyles["minWidth"]) + let maxWidth = extractPixels(responsiveStyles["maxWidth"]) + + let borderRadius = extractPixels(responsiveStyles["borderRadius"]) ?? 0 + + // 2. Build base layout (wrapped or not) + let layoutView: some View = Group { + if wrap { + LazyVGrid( + columns: [GridItem(.adaptive(minimum: 100), spacing: spacing)], + alignment: BuilderBlockLayout.horizontalAlignment( + marginsLeft: marginLeft, marginsRight: marginRight, justify: justify, + alignItems: alignItems), + spacing: spacing, + content: content + ) + } else if direction == "row" { + HStack( + alignment: BuilderBlockLayout.verticalAlignment( + justify: justify, alignItems: alignItems), spacing: spacing + ) { + content() + } + } else { + VStack( + alignment: BuilderBlockLayout.horizontalAlignment( + marginsLeft: marginLeft, marginsRight: marginRight, justify: justify, + alignItems: alignItems), spacing: spacing + ) { + content() + } + } + } + + // 3. Wrap in scroll if overflow: auto + let scrollableView: some View = Group { + if scroll { + ScrollView(.horizontal, showsIndicators: false) { + layoutView + } + } else { + layoutView + } + } + + // 4. Apply visual and layout modifiers + return + scrollableView + .padding(padding) + .frame(minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight) + .padding(margin) //margin + .cornerRadius(borderRadius) + } + + func extractPixels(_ value: String?) -> CGFloat? { + guard let value = value?.replacingOccurrences(of: "px", with: ""), + let number = Double(value) + else { return nil } + return CGFloat(number) + } + + func extractEdgeInsets(for insetType: String, from styles: [String: String]) -> EdgeInsets { + + return EdgeInsets( + top: extractPixels(styles["\(insetType)Top"]) ?? 0, + leading: extractPixels(styles["\(insetType)Left"]) ?? 0, + bottom: extractPixels(styles["\(insetType)Bottom"]) ?? 0, + trailing: extractPixels(styles["\(insetType)Right"]) ?? 0 + ) + } + + static func horizontalAlignment( + marginsLeft: String?, marginsRight: String?, justify: String?, alignItems: String? + ) -> HorizontalAlignment { + + if (marginsLeft == "auto" && marginsRight == "auto") || justify == "center" + || alignItems == "center" + { + return .center + } else if marginsLeft == "auto" || justify == "flex-start" || alignItems == "flex-start" { + return .leading + } else if marginsRight == "auto" || justify == "flex-end" || alignItems == "flex-end" { + return .trailing + } + return .center + } + + static func verticalAlignment(justify: String?, alignItems: String?) -> VerticalAlignment { + + if justify == "center" || alignItems == "center" { + return .center + } else if justify == "flex-start" || alignItems == "flex-start" { + return .top + } else if justify == "flex-end" || alignItems == "flex-end" { + return .bottom + } + return .center + } + +} diff --git a/Sources/BuilderIO/Components/BuilderColumns.swift b/Sources/BuilderIO/Components/BuilderColumns.swift index 2feb58d..3d33c65 100644 --- a/Sources/BuilderIO/Components/BuilderColumns.swift +++ b/Sources/BuilderIO/Components/BuilderColumns.swift @@ -36,7 +36,7 @@ struct BuilderColumns: BuilderViewProtocol { VStack(spacing: space) { ForEach(columns.indices, id: \.self) { index in - BuilderModel(blocks: columns[index].blocks) + BuilderBlock(blocks: columns[index].blocks) } } .padding( diff --git a/Sources/BuilderIO/Components/BuilderComponentProtocol.swift b/Sources/BuilderIO/Components/BuilderComponentProtocol.swift index 0a6a7f7..5269233 100644 --- a/Sources/BuilderIO/Components/BuilderComponentProtocol.swift +++ b/Sources/BuilderIO/Components/BuilderComponentProtocol.swift @@ -60,12 +60,6 @@ struct ResponsiveStylesBuilderView: ViewModifier { idealWidth: horizontalAlignmentFrame.idealWidth, maxWidth: horizontalAlignmentFrame.maxWidth, alignment: horizontalAlignmentFrame.alignment ) - .if(fontSize != nil) { view in - view.font(.system(size: fontSize!).weight(fontWeight!)) - } - .if(foregroundColor != nil) { view in - view.foregroundColor(foregroundColor) - } } diff --git a/Sources/BuilderIO/Components/BuilderImage.swift b/Sources/BuilderIO/Components/BuilderImage.swift index b80131b..a5291ee 100644 --- a/Sources/BuilderIO/Components/BuilderImage.swift +++ b/Sources/BuilderIO/Components/BuilderImage.swift @@ -16,7 +16,6 @@ struct BuilderImage: BuilderViewProtocol { var body: some View { AsyncImage(url: imageURL).frame(width: 5, height: 5) - EmptyView() } } diff --git a/Sources/BuilderIO/Components/BuilderModel.swift b/Sources/BuilderIO/Components/BuilderModel.swift deleted file mode 100644 index a9b0a6a..0000000 --- a/Sources/BuilderIO/Components/BuilderModel.swift +++ /dev/null @@ -1,61 +0,0 @@ -import SwiftUI -import SwiftyJSON - -//BuilderBox forms the out layout container for all components mimicking Blocks from response. As blocks can have layout direction of either horizontal or vertical a check is made and layout selected. - -struct BuilderModel: View { - - var blocks: [BuilderBlockModel] - var componentType: BuilderComponentType = .box - - init(blocks: [BuilderBlockModel]) { - self.blocks = blocks - } - - var body: some View { - - ScrollView { - ForEach(Array(blocks.enumerated()), id: \.offset) { index, child in - let responsiveStyles = CSSStyleUtil.getFinalStyle(responsiveStyles: child.responsiveStyles) - - //Calculate the layout direction based on the responsive styles - let isHorizontal = - (responsiveStyles["flexDirection"] == CSSConstants.FlexDirection.row.rawValue) - let layout = - isHorizontal - ? AnyLayout(HStackLayout(alignment: .center)) - : AnyLayout(VStackLayout(alignment: .center)) - - Group { - if isHorizontal { - ScrollView(.horizontal, showsIndicators: false) { - layout { - layoutContent(for: child) - }.modifier( - ResponsiveStylesBuilderView( - responsiveStyles: responsiveStyles ?? [:], isText: false)) - } - } else { - layout { - layoutContent(for: child) - }.modifier( - ResponsiveStylesBuilderView(responsiveStyles: responsiveStyles ?? [:], isText: false)) - } - } - - } - } - } - - @ViewBuilder - private func layoutContent(for child: BuilderBlockModel) -> some View { - if let component = child.component { - BuilderComponentRegistry.shared.view(for: child) - } else if let children = child.children, !children.isEmpty { - BuilderModel(blocks: children) - } else { - Spacer() - } - } - -} diff --git a/Sources/BuilderIO/Components/BuilderSection.swift b/Sources/BuilderIO/Components/BuilderSection.swift index 108b3c3..4a79295 100644 --- a/Sources/BuilderIO/Components/BuilderSection.swift +++ b/Sources/BuilderIO/Components/BuilderSection.swift @@ -28,7 +28,7 @@ struct BuilderSection: BuilderViewProtocol { VStack(spacing: space) { ForEach(0...columns.count - 1, id: \.self) { index in - BuilderModel(blocks: columns[index].blocks) + BuilderBlock(blocks: columns[index].blocks) } } diff --git a/Sources/BuilderIO/Components/BuilderText.swift b/Sources/BuilderIO/Components/BuilderText.swift index 1563b77..bc82fc5 100644 --- a/Sources/BuilderIO/Components/BuilderText.swift +++ b/Sources/BuilderIO/Components/BuilderText.swift @@ -17,9 +17,6 @@ struct BuilderText: BuilderViewProtocol { var body: some View { Text(CSSStyleUtil.getTextWithoutHtml(text ?? "")) - .if(!(self.responsiveStyles?.isEmpty ?? true)) { view in - view.responsiveStylesBuilderView(responsiveStyles: self.responsiveStyles!, isText: true) - } } } diff --git a/Sources/BuilderIO/Helpers/CSSStyleUtil.swift b/Sources/BuilderIO/Helpers/CSSStyleUtil.swift index 32f4dd0..7af5930 100644 --- a/Sources/BuilderIO/Helpers/CSSStyleUtil.swift +++ b/Sources/BuilderIO/Helpers/CSSStyleUtil.swift @@ -1,7 +1,7 @@ import Foundation import SwiftUI -enum HorizontalAlignment { +enum BuilderHorizontalAlignment { case FullWidth case Center case LeftAlign @@ -186,7 +186,9 @@ class CSSStyleUtil { return "" } - static func getHorizontalAlignmentFromMargin(styles: [String: String]) -> HorizontalAlignment { + static func getHorizontalAlignmentFromMargin(styles: [String: String]) + -> BuilderHorizontalAlignment + { let marginLeft = styles["marginLeft"] let marginRight = styles["marginRight"] @@ -198,29 +200,31 @@ class CSSStyleUtil { let isMarginRightAuto = marginRight?.lowercased() == "auto" if isMarginLeftAuto && isMarginRightAuto { - return HorizontalAlignment.Center + return BuilderHorizontalAlignment.Center } else if isMarginLeftAuto { - return HorizontalAlignment.RightAlign + return BuilderHorizontalAlignment.RightAlign } else if isMarginRightAuto { - return HorizontalAlignment.LeftAlign + return BuilderHorizontalAlignment.LeftAlign } else if isMarginLeftAbsentOrZero && isMarginRightAbsentOrZero { - return HorizontalAlignment.FullWidth + return BuilderHorizontalAlignment.FullWidth } // Default full width? - return HorizontalAlignment.FullWidth + return BuilderHorizontalAlignment.FullWidth } - static func getHorizontalAlignmentFromAlignSelf(styles: [String: String]) -> HorizontalAlignment { + static func getHorizontalAlignmentFromAlignSelf(styles: [String: String]) + -> BuilderHorizontalAlignment + { let alignSelf = styles["alignSelf"] if alignSelf == "center" { - return HorizontalAlignment.FullWidth + return BuilderHorizontalAlignment.FullWidth } else if alignSelf == "auto" || alignSelf == "stretch" { - return HorizontalAlignment.FullWidth + return BuilderHorizontalAlignment.FullWidth } - return HorizontalAlignment.FullWidth + return BuilderHorizontalAlignment.FullWidth } - static func getHorizontalAlignment(styles: [String: String]) -> HorizontalAlignment { + static func getHorizontalAlignment(styles: [String: String]) -> BuilderHorizontalAlignment { if styles["alignSelf"] != nil { return getHorizontalAlignmentFromAlignSelf(styles: styles) } else { @@ -232,21 +236,21 @@ class CSSStyleUtil { static func getFrameFromHorizontalAlignment(styles: [String: String], isText: Bool) -> FrameDimensions { - var horizontalAlignment: HorizontalAlignment + var horizontalAlignment: BuilderHorizontalAlignment if styles["alignSelf"] != nil { horizontalAlignment = getHorizontalAlignmentFromAlignSelf(styles: styles) } else { horizontalAlignment = getHorizontalAlignmentFromMargin(styles: styles) } - if horizontalAlignment == HorizontalAlignment.FullWidth { + if horizontalAlignment == BuilderHorizontalAlignment.FullWidth { return FrameDimensions( minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, alignment: isText ? .leading : .center) - } else if horizontalAlignment == HorizontalAlignment.Center { + } else if horizontalAlignment == BuilderHorizontalAlignment.Center { return FrameDimensions(alignment: .center) - } else if horizontalAlignment == HorizontalAlignment.LeftAlign { + } else if horizontalAlignment == BuilderHorizontalAlignment.LeftAlign { return FrameDimensions(alignment: .leading) } else { // Right align diff --git a/Sources/BuilderIO/Helpers/RenderContent.swift b/Sources/BuilderIO/Helpers/RenderContent.swift index 9808553..2d84b5d 100644 --- a/Sources/BuilderIO/Helpers/RenderContent.swift +++ b/Sources/BuilderIO/Helpers/RenderContent.swift @@ -20,16 +20,14 @@ public struct RenderContent: View { } public var body: some View { - VStack(alignment: .leading, spacing: 0) { - BuilderModel(blocks: content.data.blocks) + ScrollView { + BuilderBlock(blocks: content.data.blocks) .onAppear { if !BuilderContentAPI.isPreviewing() { sendTrackingPixel() } } } - .frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity) - .background(Color.white) } func sendTrackingPixel() {