From b8a961de331569dc52eeec8bfdb27140159580ef Mon Sep 17 00:00:00 2001 From: Praveen P Date: Mon, 9 Oct 2023 10:41:06 -0400 Subject: [PATCH] Redesign Package Structure. Added BorderStyleModifier via BezierPathShape (#19) * Background Style * Color Style * Theme model and Background, Color Styles * Re-design Package Structure. Added BorderStyleModifier via BezierPathShape --------- Co-authored-by: PraveenP --- .../ExampleApp.xcodeproj/project.pbxproj | 18 +-- ExampleApp/ExampleApp/Resources/Theme.json | 26 +++- ExampleApp/ExampleApp/Views/ExTextView.swift | 6 +- Package.swift | 11 +- .../Extenstions/Color+Extension.swift | 6 + .../Extenstions/Data+Extension.swift | 2 +- .../Extenstions/Font+Extenstion.swift | 0 .../Models/ColorSchemeValue.swift | 2 +- Sources/Theme/Models/RectCorner.swift | 20 +++ Sources/Theme/Models/ThemeJSONStructure.swift | 60 +++++++++ Sources/Theme/Models/ThemeModel.swift | 57 ++++++--- Sources/Theme/Models/ThemeStructure.swift | 33 ----- Sources/Theme/ThemesManager.swift | 8 +- Sources/Theme/Utils/BezierPathShape.swift | 64 ++++++++++ .../Theme/Utils/EdgeInsets+Extension.swift | 42 +++++++ Sources/Theme/Utils/UserCache.swift | 66 ++++++++++ Sources/Theme/Utils/ViewState.swift | 23 ++++ .../ViewModifiers/BorderStyleModifier.swift | 118 ++++++++++++++++++ .../ViewModifiers/ColorModifier.swift | 11 +- .../ViewModifiers/ColorSchemeModifier.swift | 2 +- .../ViewModifiers/FontModifier.swift | 2 +- .../ViewModifiers/GradientModifier.swift | 49 ++++++++ .../Theme/ViewModifiers/ThemeModifier.swift | 14 ++- .../ColorTests.swift | 1 - 24 files changed, 552 insertions(+), 89 deletions(-) rename Sources/{Core => Theme}/Extenstions/Color+Extension.swift (91%) rename Sources/{Core => Theme}/Extenstions/Data+Extension.swift (97%) rename Sources/{Core => Theme}/Extenstions/Font+Extenstion.swift (100%) rename Sources/{Core => Theme}/Models/ColorSchemeValue.swift (97%) create mode 100644 Sources/Theme/Models/RectCorner.swift create mode 100644 Sources/Theme/Models/ThemeJSONStructure.swift delete mode 100644 Sources/Theme/Models/ThemeStructure.swift create mode 100644 Sources/Theme/Utils/BezierPathShape.swift create mode 100644 Sources/Theme/Utils/EdgeInsets+Extension.swift create mode 100644 Sources/Theme/Utils/UserCache.swift create mode 100644 Sources/Theme/Utils/ViewState.swift create mode 100644 Sources/Theme/ViewModifiers/BorderStyleModifier.swift rename Sources/{Core => Theme}/ViewModifiers/ColorModifier.swift (74%) rename Sources/{Core => Theme}/ViewModifiers/ColorSchemeModifier.swift (98%) rename Sources/{Core => Theme}/ViewModifiers/FontModifier.swift (98%) create mode 100644 Sources/Theme/ViewModifiers/GradientModifier.swift rename Tests/{CoreTests => ThemeTests}/ColorTests.swift (92%) diff --git a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj index 773c4b7..5d26846 100644 --- a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj +++ b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -12,7 +12,7 @@ DE00F3D328C457C3009C7AE5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE00F3D228C457C3009C7AE5 /* Preview Assets.xcassets */; }; DE28812228D566E6009E632C /* Theme.json in Resources */ = {isa = PBXBuildFile; fileRef = DE28812028D566E6009E632C /* Theme.json */; }; DE28812328D566E6009E632C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE28812128D566E6009E632C /* Assets.xcassets */; }; - DE4E0FE529D8A1FA00CD1AA8 /* MobileTheme in Frameworks */ = {isa = PBXBuildFile; productRef = DE4E0FE429D8A1FA00CD1AA8 /* MobileTheme */; }; + DE99544229D9196D00F0C2A8 /* MobileTheme in Frameworks */ = {isa = PBXBuildFile; productRef = DE99544129D9196D00F0C2A8 /* MobileTheme */; }; DE9F65F328D6BB2C00DA9989 /* NavigationContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9F65F228D6BB2C00DA9989 /* NavigationContentView.swift */; }; DED6F28728D6A88C00AB7E5D /* ExTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED6F28628D6A88C00AB7E5D /* ExTextView.swift */; }; /* End PBXBuildFile section */ @@ -26,9 +26,9 @@ DE00F3FC28C48378009C7AE5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; DE28812028D566E6009E632C /* Theme.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Theme.json; sourceTree = ""; }; DE28812128D566E6009E632C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DE4E0FE329D8A14B00CD1AA8 /* MobileCore-SwiftUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "MobileCore-SwiftUI"; path = ..; sourceTree = ""; }; DE4E0FE629D910FB00CD1AA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../README.md; sourceTree = ""; }; DE99543F29D9131800F0C2A8 /* ExampleApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ExampleApp.xctestplan; sourceTree = ""; }; + DE99544029D9194200F0C2A8 /* MobileTheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MobileTheme; path = ..; sourceTree = ""; }; DE9F65F228D6BB2C00DA9989 /* NavigationContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationContentView.swift; sourceTree = ""; }; DED6F28628D6A88C00AB7E5D /* ExTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExTextView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -38,7 +38,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DE4E0FE529D8A1FA00CD1AA8 /* MobileTheme in Frameworks */, + DE99544229D9196D00F0C2A8 /* MobileTheme in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,7 +90,7 @@ DE00F3F628C457D5009C7AE5 /* Packages */ = { isa = PBXGroup; children = ( - DE4E0FE329D8A14B00CD1AA8 /* MobileCore-SwiftUI */, + DE99544029D9194200F0C2A8 /* MobileTheme */, ); name = Packages; sourceTree = ""; @@ -138,7 +138,7 @@ ); name = ExampleApp; packageProductDependencies = ( - DE4E0FE429D8A1FA00CD1AA8 /* MobileTheme */, + DE99544129D9196D00F0C2A8 /* MobileTheme */, ); productName = ExampleApp; productReference = DE00F3C828C457C1009C7AE5 /* ExampleApp.app */; @@ -160,7 +160,7 @@ }; }; buildConfigurationList = DE00F3C328C457C1009C7AE5 /* Build configuration list for PBXProject "ExampleApp" */; - compatibilityVersion = "Xcode 13.0"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -357,6 +357,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ExampleApp/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -386,6 +387,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ExampleApp/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -424,7 +426,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - DE4E0FE429D8A1FA00CD1AA8 /* MobileTheme */ = { + DE99544129D9196D00F0C2A8 /* MobileTheme */ = { isa = XCSwiftPackageProductDependency; productName = MobileTheme; }; diff --git a/ExampleApp/ExampleApp/Resources/Theme.json b/ExampleApp/ExampleApp/Resources/Theme.json index 5960798..1b344aa 100644 --- a/ExampleApp/ExampleApp/Resources/Theme.json +++ b/ExampleApp/ExampleApp/Resources/Theme.json @@ -2,19 +2,35 @@ "version": "1.0", "colors": { "white": "#FFFFFF", - "red": "#EE4B2B", - "blue": "#004CFF" + "primayBlue": "#2673DD,,#EE2C4A", + "secondaryBlue": "#2673dd", + "red": "#EE2C4A,,#A82830", + "green": "#44CC77,,#309053", + "warning": "#FFBB00,,#B2B400", + "pink": "#F9DAE0,,#EC455B", + "whiteBackground": "#F0F2F5,,#121212", + "panel": "#FAFAFA,,#222222", + "border": "#E8E8E8,,#121212", + "black": "#000000,,#FFFFFF" }, "fonts": { "title": { "weight": "title" } }, "styles": { "TitleRW": { - "forgroundColor": {"light": "red", "dark": "white"}, - "font": "title" + "forgroundColor": "black", + "font": "title", + "background": { + "color": "pink", + "border": { + "color": "green", + "radius": [10], + "thickness": 20 + } + } }, "BodyBR": { - "forgroundColor": {"light": "blue", "dark": "red"}, + "forgroundColor": "primayBlue", "font": "body" } } diff --git a/ExampleApp/ExampleApp/Views/ExTextView.swift b/ExampleApp/ExampleApp/Views/ExTextView.swift index ef6f445..621931a 100644 --- a/ExampleApp/ExampleApp/Views/ExTextView.swift +++ b/ExampleApp/ExampleApp/Views/ExTextView.swift @@ -5,7 +5,6 @@ // Created by Praveen Prabhakar on 17/09/22. // -import Core import SwiftUI import Theme @@ -24,9 +23,10 @@ struct ExTextView: View { .theme(Constants.themeFont) Text("Font as 'title' in LightMode and 'headline' in DarkMode") .theme(Constants.themeFont) - Text("'Red' in LightMode and 'White' in DarkMode") + Text("Text 'Black' in LightMode and 'White' in DarkMode") + .padding() .style(Constants.rwTitleStyle) - Text("'Blue' in LightMode and 'Red' in DarkMode") + Text("Text 'Blue' in LightMode and 'Red' in DarkMode") .style(Constants.brBodyStyle) Spacer() } diff --git a/Package.swift b/Package.swift index 5be59b0..6be0af6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,19 +5,16 @@ import PackageDescription let package = Package( name: "MobileTheme", - platforms: [.iOS(.v13), .macOS(.v10_15)], + platforms: [.iOS(.v14), .macOS(.v11)], products: [ - .library(name: "MobileTheme", targets: ["Theme", "Core"]) + .library(name: "MobileTheme", targets: ["Theme"]) ], dependencies: [ // .package(url: "https://github.com/apple/swift-docc-plugin", branch: "main") ], targets: [ // Theme - .target(name: "Theme", dependencies: ["Core"]), - .testTarget(name: "ThemeTests", dependencies: ["Theme"]), - // Extensions - .target(name: "Core"), - .testTarget(name: "CoreTests", dependencies: ["Core"]) + .target(name: "Theme"), + .testTarget(name: "ThemeTests", dependencies: ["Theme"]) ] ) diff --git a/Sources/Core/Extenstions/Color+Extension.swift b/Sources/Theme/Extenstions/Color+Extension.swift similarity index 91% rename from Sources/Core/Extenstions/Color+Extension.swift rename to Sources/Theme/Extenstions/Color+Extension.swift index 8e951d2..2113f54 100644 --- a/Sources/Core/Extenstions/Color+Extension.swift +++ b/Sources/Theme/Extenstions/Color+Extension.swift @@ -8,6 +8,12 @@ import Foundation import SwiftUI +public extension String { + func getColor() -> Color { + Color(hex: self) + } +} + public extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) diff --git a/Sources/Core/Extenstions/Data+Extension.swift b/Sources/Theme/Extenstions/Data+Extension.swift similarity index 97% rename from Sources/Core/Extenstions/Data+Extension.swift rename to Sources/Theme/Extenstions/Data+Extension.swift index 4ae6931..1d4f887 100644 --- a/Sources/Core/Extenstions/Data+Extension.swift +++ b/Sources/Theme/Extenstions/Data+Extension.swift @@ -1,6 +1,6 @@ // // Data+Extension.swift -// Core +// Theme // // Created by Praveen Prabhakar on 16/09/22. // diff --git a/Sources/Core/Extenstions/Font+Extenstion.swift b/Sources/Theme/Extenstions/Font+Extenstion.swift similarity index 100% rename from Sources/Core/Extenstions/Font+Extenstion.swift rename to Sources/Theme/Extenstions/Font+Extenstion.swift diff --git a/Sources/Core/Models/ColorSchemeValue.swift b/Sources/Theme/Models/ColorSchemeValue.swift similarity index 97% rename from Sources/Core/Models/ColorSchemeValue.swift rename to Sources/Theme/Models/ColorSchemeValue.swift index 9ff19e8..8be9008 100644 --- a/Sources/Core/Models/ColorSchemeValue.swift +++ b/Sources/Theme/Models/ColorSchemeValue.swift @@ -1,6 +1,6 @@ // // ColorSchemeValue.swift -// Core +// Theme // // Created by Praveen Prabhakar on 16/09/22. // diff --git a/Sources/Theme/Models/RectCorner.swift b/Sources/Theme/Models/RectCorner.swift new file mode 100644 index 0000000..f983238 --- /dev/null +++ b/Sources/Theme/Models/RectCorner.swift @@ -0,0 +1,20 @@ +// +// RectCorner.swift +// Theme +// +// Created by Praveen P on 10/9/23. +// + +import Foundation +import SwiftUI + +struct RectCorner: OptionSet { + let rawValue: Int + + static let topLeft = RectCorner(rawValue: 1 << 0) + static let topRight = RectCorner(rawValue: 1 << 1) + static let bottomRight = RectCorner(rawValue: 1 << 2) + static let bottomLeft = RectCorner(rawValue: 1 << 3) + + static let allCorners: RectCorner = [.topLeft, topRight, .bottomLeft, .bottomRight] +} diff --git a/Sources/Theme/Models/ThemeJSONStructure.swift b/Sources/Theme/Models/ThemeJSONStructure.swift new file mode 100644 index 0000000..24d3d21 --- /dev/null +++ b/Sources/Theme/Models/ThemeJSONStructure.swift @@ -0,0 +1,60 @@ +// +// ThemeJSONStructure.swift +// Theme +// +// Created by Praveen Prabhakar on 16/09/22. +// + +import Foundation +import SwiftUI + +struct ThemeJSONStructure: Codable { + struct FontStyle: Codable { + var size: CGFloat? + /// Based on ``Font/Weight`` + var weight: String? + /// Based on ``Font/TextStyle`` + var styleName: String? + } + + struct ColorStyle: Codable { + var light: String + var dark: String? + } + + struct UserStyle: Codable { + var forgroundColor: String? + var font: String? + var background: BackgroundStyle? + } + + struct BackgroundStyle: Codable { + var color: String? + var ignoringSafeArea: Bool? + var gradient: StyleGradient? + var border: StyleBorder? + } + + struct StyleGradient: Codable { + var colors: [String] + var locations: [CGFloat]? + } + + struct StyleBorder: Codable { + var radius: [CGFloat]? + var thickness: Int? + var color: String? + + var borderColor: Color? { + color?.getColor() + } + } + + var colors: [String: String]? + var fonts: [String: FontStyle]? + var styles: [String: UserStyle]? +} + +enum Alignment: String, Codable { + case left, center, right +} diff --git a/Sources/Theme/Models/ThemeModel.swift b/Sources/Theme/Models/ThemeModel.swift index 336b359..444d8c4 100644 --- a/Sources/Theme/Models/ThemeModel.swift +++ b/Sources/Theme/Models/ThemeModel.swift @@ -5,33 +5,31 @@ // Created by Praveen Prabhakar on 11/09/22. // -import Core import SwiftUI public class ThemeModel { - var colors = [String: Color]() + var colors = [String: ColorSchemeValue]() var fonts = [String: Font]() var styles = [String: UserStyle]() struct UserStyle { var forgroundColor: ColorSchemeValue? + var backgroundColor: StyleBackground? var font: ColorSchemeValue? + } - init(fcLight: Color? = nil, fcDark: Color? = nil, font: Font? = nil) { - if let fcLight = fcLight { - self.forgroundColor = ColorSchemeValue(fcLight, dark: fcDark) - } - if let fLight = font { - self.font = ColorSchemeValue(fLight, dark: nil) - } - } + struct StyleBackground { + var color: ColorSchemeValue? + var ignoringSafeArea: Bool? + var gradient: ThemeJSONStructure.StyleGradient? + var border: ThemeJSONStructure.StyleBorder? } } /// Generate ``ThemeModel`` based on `json Data` extension ThemeModel { static func generateModel(_ jsonData: Data) throws -> ThemeModel { - let theme = try JSONDecoder().decode(ThemeStructure.self, from: jsonData) + let theme = try JSONDecoder().decode(ThemeJSONStructure.self, from: jsonData) let model = ThemeModel() // Generate Colors theme.colors?.forEach { model.colors[$0] = Color.style($1) } @@ -44,17 +42,30 @@ extension ThemeModel { /// Generate ``ThemeModel/UserStyle`` based on ``ThemeStructure.UserStyle`` private static - func style(_ style: ThemeStructure.UserStyle, model: ThemeModel) -> UserStyle? { - let (fcLight, fcDark) = (model.colors[style.forgroundColor?.light ?? ""], - model.colors[style.forgroundColor?.dark ?? ""]) - let font = model.fonts[style.font ?? ""] - return UserStyle(fcLight: fcLight, fcDark: fcDark, font: font) + func style(_ style: ThemeJSONStructure.UserStyle, model: ThemeModel) -> UserStyle? { + // Colors + let fgColor = model.colors[style.forgroundColor ?? ""] + let bgLight = model.colors[style.background?.color ?? ""] + // BackGround Style + let backgroundStyle = StyleBackground( + color: bgLight, + ignoringSafeArea: style.background?.ignoringSafeArea, + gradient: style.background?.gradient + ) + // User Style Setup + var userStyleValue = UserStyle(forgroundColor: fgColor, backgroundColor: backgroundStyle) + + // Fonts + if let font = model.fonts[style.font ?? ""] { + userStyleValue.font = ColorSchemeValue(font, dark: nil) + } + return userStyleValue } } /// Generate ``Font`` based on ``ThemeStructure.FontStyle`` extension Font { - static func style(_ style: ThemeStructure.FontStyle) -> Font? { + static func style(_ style: ThemeJSONStructure.FontStyle) -> Font? { /// Generate ``Font`` based on StyleName ``Font/TextStyle`` if let styleName = style.styleName, let font = Font.fromStyleName(styleName: styleName) { @@ -70,9 +81,17 @@ extension Font { /// Generate ``Color`` based on `hex color` extension Color { - static func style(_ name: String) -> Color? { + static func style(_ name: String) -> ColorSchemeValue? { if name.hasPrefix("#") { - return Color(hex: name) + let colorNames = name.components(separatedBy: ",,") + guard let light = colorNames.first else { + return nil + } + var colors = ColorSchemeValue(Color(hex: light)) + if let dark = colorNames.last { + colors.dark = Color(hex: dark) + } + return colors } return nil } diff --git a/Sources/Theme/Models/ThemeStructure.swift b/Sources/Theme/Models/ThemeStructure.swift deleted file mode 100644 index 5b5f267..0000000 --- a/Sources/Theme/Models/ThemeStructure.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ThemeStructure.swift -// Theme -// -// Created by Praveen Prabhakar on 16/09/22. -// - -import Foundation -import SwiftUI - -struct ThemeStructure: Codable { - struct FontStyle: Codable { - var size: CGFloat? - /// Based on ``Font/Weight`` - var weight: String? - /// Based on ``Font/TextStyle`` - var styleName: String? - } - - struct ColorStyle: Codable { - var light: String - var dark: String? - } - - struct UserStyle: Codable { - var forgroundColor: ColorStyle? - var font: String? - } - - var colors: [String: String]? - var fonts: [String: FontStyle]? - var styles: [String: UserStyle]? -} diff --git a/Sources/Theme/ThemesManager.swift b/Sources/Theme/ThemesManager.swift index 5b74564..f81ed03 100644 --- a/Sources/Theme/ThemesManager.swift +++ b/Sources/Theme/ThemesManager.swift @@ -29,10 +29,10 @@ extension ThemesManager { /// Call this function to get `ColorSchemeValue: Color` /// Parameters: /// - style: Name of the style to fetch -/// - Returns: `Color` - static func color(_ name: String) -> Color? { - Self.shared.themeModel?.colors[name] - } +/// - Returns: `Color` +/// static func color(_ name: String) -> Color? { +/// Self.shared.themeModel?.colors[name] +/// } /// Call this function to get `ColorSchemeValue: Font` /// diff --git a/Sources/Theme/Utils/BezierPathShape.swift b/Sources/Theme/Utils/BezierPathShape.swift new file mode 100644 index 0000000..62a0228 --- /dev/null +++ b/Sources/Theme/Utils/BezierPathShape.swift @@ -0,0 +1,64 @@ +// +// BezierPathShape.swift +// Theme +// +// Created by Praveen Prabhakar on 28/03/23. +// +import SwiftUI + +/* + public struct BezierPathShape: Shape { + public var radius = CGFloat.infinity + public var corners = RectCorner.allCorners + + public func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} +*/ + +// draws shape with specified rounded corners applying corner radius +struct BezierPathShape: Shape { + + var radius: CGFloat = .zero + var corners: RectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + var path = Path() + + let p1 = CGPoint(x: rect.minX, y: corners.contains(.topLeft) ? rect.minY + radius : rect.minY ) + let p2 = CGPoint(x: corners.contains(.topLeft) ? rect.minX + radius : rect.minX, y: rect.minY ) + + let p3 = CGPoint(x: corners.contains(.topRight) ? rect.maxX - radius : rect.maxX, y: rect.minY ) + let p4 = CGPoint(x: rect.maxX, y: corners.contains(.topRight) ? rect.minY + radius : rect.minY ) + + let p5 = CGPoint(x: rect.maxX, y: corners.contains(.bottomRight) ? rect.maxY - radius : rect.maxY ) + let p6 = CGPoint(x: corners.contains(.bottomRight) ? rect.maxX - radius : rect.maxX, y: rect.maxY ) + + let p7 = CGPoint(x: corners.contains(.bottomLeft) ? rect.minX + radius : rect.minX, y: rect.maxY ) + let p8 = CGPoint(x: rect.minX, y: corners.contains(.bottomLeft) ? rect.maxY - radius : rect.maxY ) + + path.move(to: p1) + path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), + tangent2End: p2, + radius: radius) + path.addLine(to: p3) + path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), + tangent2End: p4, + radius: radius) + path.addLine(to: p5) + path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), + tangent2End: p6, + radius: radius) + path.addLine(to: p7) + path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), + tangent2End: p8, + radius: radius) + path.closeSubpath() + + return path + } +} diff --git a/Sources/Theme/Utils/EdgeInsets+Extension.swift b/Sources/Theme/Utils/EdgeInsets+Extension.swift new file mode 100644 index 0000000..ac4911d --- /dev/null +++ b/Sources/Theme/Utils/EdgeInsets+Extension.swift @@ -0,0 +1,42 @@ +// +// EdgeInsets+Extension.swift +// Theme +// +// Created by Praveen Prabhakar on 19/05/23. +// + +import SwiftUI + +public extension EdgeInsets { + static var zeroInsets: EdgeInsets { + EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + } + + static var padding: EdgeInsets { + EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 20) + } + + static var paddingContentView: EdgeInsets { + EdgeInsets(top: 15, leading: 20, bottom: 0, trailing: 20) + } + + static var padding16: EdgeInsets { + .init(16) + } + + static var paddingRegular: EdgeInsets { + EdgeInsets(top: 12, leading: 24, bottom: 12, trailing: 24) + } + + static var paddingTop15: EdgeInsets { + .init(top: 15) + } + + init(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) { + self.init(top: top, leading: left, bottom: bottom, trailing: right) + } + + init(_ all: CGFloat = 0) { + self.init(top: all, leading: all, bottom: all, trailing: all) + } +} diff --git a/Sources/Theme/Utils/UserCache.swift b/Sources/Theme/Utils/UserCache.swift new file mode 100644 index 0000000..83c01a5 --- /dev/null +++ b/Sources/Theme/Utils/UserCache.swift @@ -0,0 +1,66 @@ +// + +import Foundation + +final class UserCache { + private let wrapped = NSCache() + + func insert(_ value: Value, forKey key: Key) { + let entry = Entry(value: value) + wrapped.setObject(entry, forKey: WrappedKey(key)) + } + + func value(forKey key: Key) -> Value? { + let entry = wrapped.object(forKey: WrappedKey(key)) + return entry?.value + } + + func removeValue(forKey key: Key) { + wrapped.removeObject(forKey: WrappedKey(key)) + } + + func clearCache() { + wrapped.removeAllObjects() + } +} + +extension UserCache { + subscript(key: Key) -> Value? { + get { return value(forKey: key) } + set { + guard let value = newValue else { + // If nil was assigned using our subscript, + // then we remove any value for that key: + removeValue(forKey: key) + return + } + insert(value, forKey: key) + } + } +} + +private extension UserCache { + final class WrappedKey: NSObject { + let key: Key + + init(_ key: Key) { self.key = key } + + override var hash: Int { return key.hashValue } + + override func isEqual(_ object: Any?) -> Bool { + guard let value = object as? WrappedKey else { + return false + } + + return value.key == key + } + } + + final class Entry { + let value: Value + + init(value: Value) { + self.value = value + } + } +} diff --git a/Sources/Theme/Utils/ViewState.swift b/Sources/Theme/Utils/ViewState.swift new file mode 100644 index 0000000..3ccf6c7 --- /dev/null +++ b/Sources/Theme/Utils/ViewState.swift @@ -0,0 +1,23 @@ +// +// ViewState.swift +// Theme +// +// Created by Praveen Prabhakar on 19/05/23. +// + +import SwiftUI + +public +protocol ObservableViewState: ObservableObject { + var state: ViewState { get } + func loadView() +} + +public enum ViewState: String { + case normal, disabled, highlighted, selected + case idle, loaded + + var value: String { + self == .normal ? "" : ":\(self.rawValue)" + } +} diff --git a/Sources/Theme/ViewModifiers/BorderStyleModifier.swift b/Sources/Theme/ViewModifiers/BorderStyleModifier.swift new file mode 100644 index 0000000..a4e0720 --- /dev/null +++ b/Sources/Theme/ViewModifiers/BorderStyleModifier.swift @@ -0,0 +1,118 @@ +// +// BorderStyleModifier.swift +// Theme +// +// Created by Praveen Prabhakar on 28/03/23. +// + +import SwiftUI + +struct BorderStyleModifier: ViewModifier { + var style: ThemeJSONStructure.StyleBorder? + + func body(content: Content) -> some View { + if style != nil { + let shape = generateShape() + if let color, let thickness { + content + .background(shape.stroke(color, lineWidth: thickness)) + .clipShape(shape) + } else { + content + .clipShape(shape) + } + } else { + content + } + } +} + +extension View { + /// Call this function to set the clip style + /// - Parameters: + /// - style: ``ThemeStructure.StyleBorder`` value from themes + /// - Returns: Modified ``View`` that incorporates the view modifier. + func borderStyle(_ borderStyle: ThemeJSONStructure.StyleBorder?) -> some View { + modifier(BorderStyleModifier(style: borderStyle)) + } +} + +private extension BorderStyleModifier { + var radius: CGFloat { + let value = style?.radius?.first { $0 > 0.0 } + return CGFloat(value ?? 0.0) + } + + var corners: RectCorner { + guard let radiusList = style?.radius, radiusList.count == 4 else { + return .allCorners + } + var corners = [RectCorner]() + if radiusList[0] > 0.0 { + corners.append(.topLeft) + } + if radiusList[1] > 0.0 { + corners.append(.topRight) + } + if radiusList[2] > 0.0 { + corners.append(.bottomLeft) + } + if radiusList[3] > 0.0 { + corners.append(.bottomRight) + } + return RectCorner(corners) + } + + var thickness: CGFloat? { + guard let thickness = style?.thickness else { + return nil + } + return CGFloat(thickness) + } + + var color: Color? { + guard let color = style?.borderColor else { + return nil + } + return color + } + + func generateShape() -> BezierPathShape { + BezierPathShape(radius: radius, corners: corners) + } +} + +// MARK: Preview + +struct BorderStyleModifier_Previews: PreviewProvider { + static let cancelStyle: ThemeJSONStructure.StyleBorder = { + var syle = ThemeJSONStructure.StyleBorder() + syle.radius = [8] + syle.color = "#EE2C4A" + syle.thickness = 20 + return syle + }() + + static let doneStyle: ThemeJSONStructure.StyleBorder = { + var syle = ThemeJSONStructure.StyleBorder() + syle.radius = [10] + syle.color = "#F9DAE0" + syle.thickness = 200 + return syle + }() + + static var previews: some View { + HStack { + Button("Cancel") { + print("") + }.padding() + .theme(.background(color: .init(.yellow, dark: .blue))) + .borderStyle(cancelStyle) + + Button("Done") { + print("") + }.padding() + .borderStyle(doneStyle) + } + } +} diff --git a/Sources/Core/ViewModifiers/ColorModifier.swift b/Sources/Theme/ViewModifiers/ColorModifier.swift similarity index 74% rename from Sources/Core/ViewModifiers/ColorModifier.swift rename to Sources/Theme/ViewModifiers/ColorModifier.swift index 7985894..6dc5971 100644 --- a/Sources/Core/ViewModifiers/ColorModifier.swift +++ b/Sources/Theme/ViewModifiers/ColorModifier.swift @@ -1,6 +1,6 @@ // // ColorModifier.swift -// Core +// Theme // // Created by Praveen Prabhakar on 11/09/22. // @@ -9,11 +9,14 @@ import SwiftUI public enum ColorModifierStyle { case foreground(color: ColorSchemeValue?) + case background(color: ColorSchemeValue?, ignoreSafeArea: Bool? = false) func value(_ colorScheme: ColorScheme) -> Color? { switch self { case let .foreground(color): return color?.value(colorScheme) + case let .background(color, _): + return color?.value(colorScheme) } } } @@ -32,6 +35,12 @@ public struct ColorModifier: ViewModifier { switch themeValue { case .foreground where color != nil: content.foregroundColor(color) + case .background(_, let ignoreSafeArea) where color != nil: + if ignoreSafeArea == true { + content.background(color.ignoresSafeArea(.all)) + } else { + content.background(color) + } default: content } diff --git a/Sources/Core/ViewModifiers/ColorSchemeModifier.swift b/Sources/Theme/ViewModifiers/ColorSchemeModifier.swift similarity index 98% rename from Sources/Core/ViewModifiers/ColorSchemeModifier.swift rename to Sources/Theme/ViewModifiers/ColorSchemeModifier.swift index 7b75906..6a54268 100644 --- a/Sources/Core/ViewModifiers/ColorSchemeModifier.swift +++ b/Sources/Theme/ViewModifiers/ColorSchemeModifier.swift @@ -1,6 +1,6 @@ // // ColorSchemeModifier.swift -// Core +// Theme // // Created by Praveen Prabhakar on 17/09/22. // diff --git a/Sources/Core/ViewModifiers/FontModifier.swift b/Sources/Theme/ViewModifiers/FontModifier.swift similarity index 98% rename from Sources/Core/ViewModifiers/FontModifier.swift rename to Sources/Theme/ViewModifiers/FontModifier.swift index f90c35d..dd50ebe 100644 --- a/Sources/Core/ViewModifiers/FontModifier.swift +++ b/Sources/Theme/ViewModifiers/FontModifier.swift @@ -1,6 +1,6 @@ // // FontModifier.swift -// Core +// Theme // // Created by Praveen Prabhakar on 12/09/22. // diff --git a/Sources/Theme/ViewModifiers/GradientModifier.swift b/Sources/Theme/ViewModifiers/GradientModifier.swift new file mode 100644 index 0000000..e4e1fab --- /dev/null +++ b/Sources/Theme/ViewModifiers/GradientModifier.swift @@ -0,0 +1,49 @@ +// +// GradientModifier.swift +// Theme +// +// Created by Praveen Prabhakar on 28/03/23. +// + +import SwiftUI + +typealias StyleGradient = ThemeJSONStructure.StyleGradient + +struct GradientModifier: ViewModifier { + var style: StyleGradient? + + func body(content: Content) -> some View { + if let style { + let gradient = Self.generateGradient(style) + content + .background(gradient) + } else { + content + } + } +} + +private extension GradientModifier { + static func getColors(_ colors: [String]) -> [Color] { + colors.map { $0.getColor() } + } + + static func generateGradient(_ style: StyleGradient) -> LinearGradient { + let colors = Self.getColors(style.colors) + if let locations = style.locations { + let stops = zip(colors, locations).map { Gradient.Stop(color: $0, location: $1) } + return LinearGradient(stops: stops, startPoint: .top, endPoint: .bottom) + } + return LinearGradient(colors: colors, startPoint: .top, endPoint: .bottom) + } +} + +extension View { + /// Call this function to set the clip style + /// - Parameters: + /// - style: ``ThemeStructure.StyleGradient`` value from themes + /// - Returns: Modified ``View`` that incorporates the view modifier. + func gradientStyle(_ gradientStyle: StyleGradient?) -> some View { + modifier(GradientModifier(style: gradientStyle)) + } +} diff --git a/Sources/Theme/ViewModifiers/ThemeModifier.swift b/Sources/Theme/ViewModifiers/ThemeModifier.swift index cf1b3ac..8bac53e 100644 --- a/Sources/Theme/ViewModifiers/ThemeModifier.swift +++ b/Sources/Theme/ViewModifiers/ThemeModifier.swift @@ -5,7 +5,6 @@ // Created by Praveen Prabhakar on 11/09/22. // -import Core import SwiftUI /// A modifier that you apply to a view or another view modifier, producing a @@ -19,6 +18,7 @@ import SwiftUI /// Text("Downtown Bus") /// .font(.title) /// .foregroundColor(Color.blue) +/// .backgroundColor(Color.blue) /// } /// } /// @@ -32,14 +32,20 @@ import SwiftUI /// public struct ThemeModifier: ViewModifier { let name: String + let viewState: ViewState @State private var themeStyle: ThemeModel.UserStyle? public func body(content: Content) -> some View { - DispatchQueue.main.async { themeStyle = ThemesManager.style(name) } + let value = "\(name)\(viewState.value)" + DispatchQueue.main.async { + themeStyle = ThemesManager.style(value) + } + let backGroundStyle = themeStyle?.backgroundColor return content .theme(themeStyle?.font) .theme(.foreground(color: themeStyle?.forgroundColor)) + .theme(.background(color: backGroundStyle?.color, ignoreSafeArea: backGroundStyle?.ignoringSafeArea)) } } @@ -49,7 +55,7 @@ public extension View { /// - Parameters: /// - name: StyleName for the element /// - Returns: Modified ``View`` that incorporates the view modifier. - func style(_ name: String) -> some View { - modifier(ThemeModifier(name: name)) + func style(_ name: String, viewState: ViewState = .normal) -> some View { + modifier(ThemeModifier(name: name, viewState: viewState)) } } diff --git a/Tests/CoreTests/ColorTests.swift b/Tests/ThemeTests/ColorTests.swift similarity index 92% rename from Tests/CoreTests/ColorTests.swift rename to Tests/ThemeTests/ColorTests.swift index a7f6d3a..ce95c1d 100644 --- a/Tests/CoreTests/ColorTests.swift +++ b/Tests/ThemeTests/ColorTests.swift @@ -7,7 +7,6 @@ import SwiftUI import XCTest -@testable import Core final class ColorTests: XCTestCase { func testHexColor() throws {