Skip to content

Added Button, Columns & Link Handling #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 0 additions & 14 deletions Sources/BuilderIO/BuilderContentWrapper.swift

This file was deleted.

55 changes: 55 additions & 0 deletions Sources/BuilderIO/BuilderIOManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SwiftUI

@MainActor
public final class BuilderIOManager: ObservableObject {
public static private(set) var shared: BuilderIOManager!

public let apiKey: String
private static var registered = false

private init(apiKey: String) {
self.apiKey = apiKey
if !Self.registered {
BuilderComponentRegistry.shared.initialize()
Self.registered = true
}
}

/// Call once during app launch
public static func configure(apiKey: String) {
guard shared == nil else {
return
}
shared = BuilderIOManager(apiKey: apiKey)
}

public func fetchBuilderPageContent(url: String) async -> Result<BuilderContent, Error> {
do {
if let content = await BuilderContentAPI.getContent(
model: "page",
apiKey: apiKey,
url: url,
locale: "",
preview: ""
) {
return .success(content)
} else {
return .failure(
NSError(
domain: "BuilderIOManager",
code: 404,
userInfo: [NSLocalizedDescriptionKey: "No content found for the given URL."]
))
}
} catch {
return .failure(error)
}
}

public func sendTrackingPixel() {
guard let url = URL(string: "https://cdn.builder.io/api/v1/pixel?apiKey=\(apiKey)") else {
return
}
URLSession.shared.dataTask(with: url).resume()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class BuilderComponentRegistry {
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)
}

Expand Down
84 changes: 34 additions & 50 deletions Sources/BuilderIO/Components/BuilderBlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftUI

//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.

@MainActor
struct BuilderBlock: View {

var blocks: [BuilderBlockModel]
Expand All @@ -13,10 +14,24 @@ struct BuilderBlock: View {

var body: some View {

ForEach(Array(blocks.enumerated()), id: \.offset) { index, child in
ForEach(blocks) { child in
let responsiveStyles = CSSStyleUtil.getFinalStyle(responsiveStyles: child.responsiveStyles)

BuilderBlockLayout(responsiveStyles: responsiveStyles ?? [:]) {
let component = child.component

//Only checking links for now, can be expanded to cover events in the future
let isTappable =
component?.name == BuilderComponentType.coreButton.rawValue
|| !(component?.options?["Link"].isEmpty ?? true) || !(child.linkUrl?.isEmpty ?? true)

let builderAction: BuilderAction? =
(isTappable)
? BuilderAction(
componentId: child.id,
options: child.component?.options,
eventActions: child.actions,
linkURL: child.linkUrl) : nil

BuilderBlockLayout(responsiveStyles: responsiveStyles ?? [:], builderAction: builderAction) {
if let component = child.component {
BuilderComponentRegistry.shared.view(for: child)
} else if let children = child.children, !children.isEmpty {
Expand All @@ -34,6 +49,9 @@ struct BuilderBlock: View {

struct BuilderBlockLayout<Content: View>: View {
let responsiveStyles: [String: String]
let builderAction: BuilderAction?
@Environment(\.buttonActionManager) private var buttonActionManager

@ViewBuilder let content: () -> Content

var body: some View {
Expand Down Expand Up @@ -80,7 +98,7 @@ struct BuilderBlockLayout<Content: View>: View {
responsiveStyles: responsiveStyles
).builderBorder(properties: BorderProperties(responsiveStyles: responsiveStyles))
} else if direction == "row" {
let hStackAlignment = BuilderBlockLayout<Content>.verticalAlignment(
let hStackAlignment = CSSAlignments.verticalAlignment(
justify: justify, alignItems: alignItems)

let frameAlignment: Alignment =
Expand All @@ -104,7 +122,7 @@ struct BuilderBlockLayout<Content: View>: View {
}
} else {

let vStackAlignment = BuilderBlockLayout<Content>.horizontalAlignment(
let vStackAlignment = CSSAlignments.horizontalAlignment(
marginsLeft: marginLeft, marginsRight: marginRight, justify: justify,
alignItems: alignItems, responsiveStyles: responsiveStyles)

Expand All @@ -118,14 +136,24 @@ struct BuilderBlockLayout<Content: View>: View {
VStack {
if marginTop == "auto" { Spacer() }

content().padding(padding)
let componentView: some View = content().padding(padding)
.frame(
minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight,
alignment: frameAlignment
).builderBackground(responsiveStyles: responsiveStyles).builderBorder(
properties: BorderProperties(responsiveStyles: responsiveStyles)
)

if let builderAction = builderAction {
Button {
buttonActionManager?.handleButtonPress(builderAction: builderAction)
} label: {
componentView
}
} else {
componentView
}

if marginBottom == "auto" { Spacer() }
}.frame(maxWidth: .infinity, alignment: frameAlignment)
}
Expand Down Expand Up @@ -178,48 +206,4 @@ struct BuilderBlockLayout<Content: View>: View {
)
}

static func horizontalAlignment(
marginsLeft: String?, marginsRight: String?, justify: String?, alignItems: String?,
responsiveStyles: [String: String]
) -> HorizontalAlignment {

if let textAlign = responsiveStyles["textAlign"] {
switch textAlign {
case "center":
return .center
case "left", "start": // "start" is also a common value in some contexts
return .leading
case "right", "end": // "end" is also a common value
return .trailing
case "justify":
break // Fall through to next checks
default:
break // Unknown textAlign value, fall through
}
}

if (marginsLeft == "auto" && marginsRight == "auto") || justify == "center"
|| alignItems == "center"
{
return .center
} else if marginsRight == "auto" || justify == "flex-start" || alignItems == "flex-start" {
return .leading
} else if marginsLeft == "auto" || justify == "flex-end" || alignItems == "flex-end" {
return .trailing
}
return .leading
}

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
}

}
81 changes: 7 additions & 74 deletions Sources/BuilderIO/Components/BuilderButton.swift
Original file line number Diff line number Diff line change
@@ -1,84 +1,17 @@
import SwiftUI
import WebKit

private typealias CSS = CSSStyleUtil
//Wrapped Text Click event handle externally at the layout level
struct BuilderButton: BuilderViewProtocol {

@available(iOS 15.0, macOS 10.15, *)
struct BuilderButton: View {
var text: String
var urlStr: String?
var openInNewTab: Bool = false
var componentType: BuilderComponentType = .coreButton

var responsiveStyles: [String: String]?
var buttonAction: ((String, String?) -> Void)?
var block: BuilderBlockModel

@State private var showWebView = false

func defaultHandleButtonClick() {
if let str = urlStr, let url = URL(string: str) {
self.showWebView = !openInNewTab
if openInNewTab == true {
UIApplication.shared.open(url)
}
}
init(block: BuilderBlockModel) {
self.block = block
}

var body: some View {
let foregroundColor = CSS.getColor(value: responsiveStyles?["color"])
let bgColor = CSS.getColor(value: responsiveStyles?["backgroundColor"])
// let _ = print("BG COLOR BUTTON ----", bgColor);
let cornerRadius = CSS.getFloatValue(cssString: responsiveStyles?["borderRadius"] ?? "0px")
let fontSize = CSS.getFloatValue(cssString: responsiveStyles?["fontSize"] ?? "16px")
let fontWeight = CSS.getFontWeightFromNumber(
value: CSS.getFloatValue(cssString: responsiveStyles?["fontWeight"] ?? "400"))
let horizontalAlignmentFrame = CSS.getFrameFromHorizontalAlignment(
styles: responsiveStyles ?? [:], isText: false)
Button(action: {
if let action = self.buttonAction {
action(text, urlStr ?? "")
} else {
self.defaultHandleButtonClick()
}

}) {
Text(CSS.getTextWithoutHtml(text))
.padding(
CSS.getBoxStyle(boxStyleProperty: "padding", finalStyles: responsiveStyles ?? [:])
) // padding for the button
.font(.system(size: fontSize).weight(fontWeight))
.frame(alignment: horizontalAlignmentFrame.alignment)

}
.frame(
idealWidth: horizontalAlignmentFrame.idealWidth, maxWidth: horizontalAlignmentFrame.maxWidth,
alignment: horizontalAlignmentFrame.alignment
)
.foregroundColor(foregroundColor)
.background(RoundedRectangle(cornerRadius: cornerRadius).fill(bgColor))

.sheet(isPresented: $showWebView) {
if let str = urlStr, let url = URL(string: str) {
WebView(url: url)
}
}
// .frame(maxWidth: .infinity)

// .frame(maxWidth: .infinity)
}
}

@available(iOS 15.0, *)
struct WebView: UIViewRepresentable {
typealias UIViewType = WKWebView

var url: URL

func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}

func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
BuilderText(block: block)
}
}
14 changes: 3 additions & 11 deletions Sources/BuilderIO/Components/BuilderColumns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,16 @@ struct BuilderColumns: BuilderViewProtocol {
self.columns = []
}

self.responsiveStyles = getFinalStyle(responsiveStyles: block.responsiveStyles)
self.space = block.component?.options?["space"].doubleValue ?? 0
}

var body: some View {
let hasBackground = responsiveStyles?["backgroundColor"] != nil
let backgroundColor = CSSStyleUtil.getColor(value: responsiveStyles?["backgroundColor"])

VStack(spacing: space) {
ForEach(columns.indices, id: \.self) { index in
BuilderBlock(blocks: columns[index].blocks)
ForEach(columns) { column in
BuilderBlock(blocks: column.blocks)
}

}
.padding(
CSSStyleUtil.getBoxStyle(boxStyleProperty: "padding", finalStyles: responsiveStyles ?? [:])
)
.background(hasBackground ? backgroundColor : nil)
.padding(
CSSStyleUtil.getBoxStyle(boxStyleProperty: "margin", finalStyles: responsiveStyles ?? [:]))
}
}
Loading