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
99 changes: 76 additions & 23 deletions Sources/DynamicUI/Extensions/View.modifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,30 @@

import SwiftUI

extension View {
/// DynamicUIModifiers
///
/// This function adds modifiers to a DynamicUIView
///
/// - Parameter modifiers: The modifiers to apply
///
/// - Returns: The modified view
public func dynamicUIModifiers(_ modifiers: [String: AnyCodable]?) -> some View {
// swiftlint:disable:previous cyclomatic_complexity
guard let modifiers = modifiers else {
return AnyView(self)
}
public struct DynamicUIModifier: ViewModifier {
/// The modifiers to apply
let modifiers: [String: AnyCodable]?
let helper = DynamicUIHelper()

let helper = DynamicUIHelper()
var tempView = AnyView(self)
// TODO: Ideally, this function would use @ViewBuilder to avoid type erasure with AnyView,
// which would improve type safety and allow for more natural SwiftUI composition.
// However, applying modifiers dynamically based on a dictionary of keys and values
// currently requires type erasure, since @ViewBuilder expects a static view hierarchy.
// Investigate approaches to apply modifiers in a type-safe way without AnyView,
// possibly by refactoring how modifiers are represented or applied.
public func body(content: Content) -> some View {
// swiftlint:disable:next cyclomatic_complexity
var tempView = AnyView(content)

modifiers.forEach { key, value in
modifiers?.forEach { key, value in
switch key {
case "foregroundStyle":
case "foregroundStyle", "foregroundColor":
guard #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *),
let string = value.toString(),
let color = helper.translateColor(string) else { break }
tempView = AnyView(tempView.foregroundStyle(color))

case "backgroundStyle":
case "backgroundStyle", "backgroundColor":
guard #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *),
let string = value.toString(),
let color = helper.translateColor(string) else { break }
Expand All @@ -52,12 +50,40 @@ extension View {
tempView = AnyView(tempView.font(.none))

case "frame":
// guard let color:
// minWidth: <#0#>, idealWidth: <#100#>, maxWidth: <#.infinity#>,
// minHeight: <#0#>, idealHeight: <#100#>, maxHeight: <#.infinity#>, alignment: <#.center#>)
// width: <#0#> height: <#0#>
guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) else { break }
tempView = AnyView(tempView.frame())
if let frameDict = value.toDictionary() {
let width = frameDict["width"]?.toDouble().map { CGFloat($0) }
let height = frameDict["height"]?.toDouble().map { CGFloat($0) }
let minWidth = frameDict["minWidth"]?.toDouble().map { CGFloat($0) }
let idealWidth = frameDict["idealWidth"]?.toDouble().map { CGFloat($0) }
let maxWidth = frameDict["maxWidth"]?.toDouble().map { CGFloat($0) }
let minHeight = frameDict["minHeight"]?.toDouble().map { CGFloat($0) }
let idealHeight = frameDict["idealHeight"]?.toDouble().map { CGFloat($0) }
let maxHeight = frameDict["maxHeight"]?.toDouble().map { CGFloat($0) }
let alignment = helper.translateAlignment(frameDict["alignment"]?.toString())

if width != nil || height != nil {
tempView = AnyView(
tempView.frame(
width: width,
height: height,
alignment: alignment
)
)
} else {
tempView = AnyView(
tempView.frame(
minWidth: minWidth,
idealWidth: idealWidth,
maxWidth: maxWidth,
minHeight: minHeight,
idealHeight: idealHeight,
maxHeight: maxHeight,
alignment: alignment
)
)
}
}

case "padding":
guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *),
Expand All @@ -77,3 +103,30 @@ extension View {
return tempView
}
}

extension View {
/// DynamicUIModifiers
///
/// This function adds modifiers to a DynamicUIView
///
/// - Parameter modifiers: The modifiers to apply
///
/// - Returns: The modified view
public func dynamicUIModifiers(_ modifiers: [String: AnyCodable]?) -> some View {
self.modifier(DynamicUIModifier(modifiers: modifiers))
}
}

#if DEBUG
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
#Preview {
Text("Test")
.dynamicUIModifiers([
"frame": .dictionary([
"width": .double(150),
"height": .double(100)
]),
"foregroundStyle": .string("red")
])
}
#endif
74 changes: 67 additions & 7 deletions Sources/DynamicUI/Helpers/AnyCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

import Foundation

/// Any Codable supports different `Codable` types as `String`, `Int`, `Data`, `Double` and `Bool`.
/// Any Codable supports different `Codable` types as `String`, `Int`, `Data`, `Double`, `Bool`,
/// and nested dictionaries `[String: AnyCodable]`.
/// This is made so you can use `AnyCodable?` in a codable struct so you can use dynamic types.
///
/// Example:
Expand All @@ -36,6 +37,9 @@ public enum AnyCodable {
/// Boolean value
case bool(Bool)

/// Dictionary value
case dictionary([String: AnyCodable])

/// No value
case none

Expand Down Expand Up @@ -97,6 +101,16 @@ extension AnyCodable {
return nil
}

/// Convert value to Dictionary
/// - Returns: value if it is a dictionary
public func toDictionary() -> [String: AnyCodable]? {
if case let .dictionary(dict) = self {
return dict
}

return nil
}

/// Check if value is nil
/// - Returns: nil if value is none/empty
public func isNil() -> Bool {
Expand All @@ -108,15 +122,16 @@ extension AnyCodable {
}
}

extension AnyCodable: Codable, Hashable {
extension AnyCodable: Codable, Equatable, Hashable {
enum CodingKeys: String, CodingKey {
case string, int, data, double, bool
case string, int, data, double, bool, dictionary
}

/// Decode the values
///
/// - Parameter decoder:
///
/// - Parameter decoder:
public init(from decoder: Decoder) throws {
// Try to decode in order of most specific/common JSON types.
if let int = try? decoder.singleValueContainer().decode(Int.self) {
self = .int(int)
return
Expand All @@ -142,13 +157,18 @@ extension AnyCodable: Codable, Hashable {
return
}

if let dict = try? decoder.singleValueContainer().decode([String: AnyCodable].self) {
self = .dictionary(dict)
return
}

// Use `self = .none` if the value can be optional
// or `throw AnyCodableError.missingValue` is it may not be optional
// or `throw AnyCodableError.missingValue` if it may not be optional
self = .none
}

/// Encode the values
///
///
/// - Parameter encoder: Encoder
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
Expand All @@ -169,8 +189,48 @@ extension AnyCodable: Codable, Hashable {
case .bool(let value):
try container.encode(value, forKey: .bool)

case .dictionary(let value):
try container.encode(value, forKey: .dictionary)

case .none:
_ = ""
}
}

public func hash(into hasher: inout Hasher) {
switch self {
case .string(let value):
hasher.combine(0)
hasher.combine(value)

case .int(let value):
hasher.combine(1)
hasher.combine(value)

case .data(let value):
hasher.combine(2)
hasher.combine(value)

case .double(let value):
hasher.combine(3)
hasher.combine(value)

case .bool(let value):
hasher.combine(4)
hasher.combine(value)

case .dictionary(let dict):
hasher.combine(5)
// Ensure deterministic hashing by sorting keys
for key in dict.keys.sorted() {
hasher.combine(key)
if let value = dict[key] {
hasher.combine(value)
}
}

case .none:
hasher.combine(6)
}
}
}
13 changes: 13 additions & 0 deletions Sources/DynamicUI/Helpers/DynamicUIHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ public class DynamicUIHelper {
return .regular
}
}

func translateAlignment(_ input: String?) -> Alignment {
switch input {
case "leading":
return .leading
case "center":
return .center
case "trailing":
return .trailing
default:
return .center
}
}
}

// swiftlint:enable cyclomatic_complexity function_body_length