├── .gitignore ├── Package.swift ├── README.md ├── Sources └── SwiftUI_CSS │ ├── Responsive.swift │ └── SwiftUI_CSS.swift └── Tests ├── LinuxMain.swift └── SwiftUI_CSSTests ├── SwiftUI_CSSTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | UserInterfaceState.xcuserstate 6 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftUI_CSS", 8 | platforms: [ 9 | // specify each minimum deployment requirement, 10 | //otherwise the platform default minimum is used. 11 | .macOS(.v10_15),.iOS(.v13) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "SwiftUI_CSS", 17 | targets: ["SwiftUI_CSS"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "SwiftUI_CSS", 28 | dependencies: []), 29 | .testTarget( 30 | name: "SwiftUI_CSSTests", 31 | dependencies: ["SwiftUI_CSS"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The missing CSS-like module for SwiftUI 2 | > Check out the [example project using SwiftUI-CSS](https://github.com/hite/SwiftUI-CSS_example); 3 | > Also, Swift Package availble which url is `https://github.com/hite/SwiftUI-CSS` 4 | > Supported macOS(.v10_14), .iOS(.v13) 5 | The SwiftUI is a great UI development framework for the iOS app. After I wrote some to-be-released app with SwiftUI framework, I realized that I need a solution to write more clear, simple, view-style-decoupled code with lots of custom style design. 6 | 7 | So here is **SwiftUI-CSS**. With **SwiftUI-CSS**, you can: 8 | 9 | ## 1. write View-style-decoupled codes 10 | *View-style-decoupled* makes your source code more clear to read, easy to refactor like html with CSS support. 11 | 12 | ### Without SwifUI-CSS: 13 | 14 | The codes to define View Structure blend into style-defined codes. 15 | 16 | ``` swift 17 | Image("image-swift") 18 | .resizable() 19 | .scaledToFit() 20 | .frame(width:100, height:100) 21 | .cornerRadius(10) 22 | .padding(EdgeInsets(top: 10, leading: 0, bottom: 15, trailing: 0)) 23 | 24 | Text("Swift") 25 | .font(.headline) 26 | .foregroundColor(Color(red: 0x33/0xff, green: 0x33/0xff, blue: 0x33/0xff)) 27 | .padding(.bottom, 10) 28 | 29 | 30 | Text("Swift is a general-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, Linux, and z/OS. ") 31 | .font(.footnote) 32 | .padding(.horizontal, 10) 33 | .foregroundColor(NormalDescColor) 34 | .lineSpacing(2) 35 | .frame(minHeight: 100, maxHeight: .infinity) 36 | ``` 37 | ### With SwifUI-CSS: 38 | 39 | 1. We divide the previous into two parts. The first part is view structures with class name: 40 | 41 | ``` swift 42 | Image("image-swift") 43 | .resizable() 44 | .scaledToFit() 45 | .addClassName(languageLogo_clsName) 46 | 47 | Text("Swift") 48 | .addClassName(languageTitle_clsName) 49 | 50 | 51 | Text("Swift is a general-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, Linux, and z/OS. ") 52 | .addClassName(languageDesc_clsName) 53 | ``` 54 | 55 | 2. The another is style definition: 56 | ``` swift 57 | let languageLogo_clsName = CSSStyle([ 58 | .width(100), 59 | .height(100), 60 | .cornerRadius(10), 61 | .paddingTLBT(10, 0, 15,0) 62 | ]) 63 | 64 | let languageTitle_clsName = CSSStyle([ 65 | .font(.headline), 66 | .foregroundColor(Color(red: 0x33/0xff, green: 0x33/0xff, blue: 0x33/0xff)), 67 | .paddingEdges([.bottom], 10) 68 | ]) 69 | 70 | let languageDesc_clsName = CSSStyle([ 71 | .font(.footnote), 72 | .paddingHorizontal(10), 73 | .foregroundColor(NormalDescColor), 74 | .lineSpacing(2), 75 | .flexHeight(min: 50, max: .infinity) 76 | ]) 77 | ``` 78 | ## 2. use module system for reuse or create a custom design system. 79 | *module system* help to reuse some common style design across the whole app which can save you to write same codes everywhere or avoid to make some mistakes. 80 | 81 | ### Without SwifUI-CSS: 82 | If you change the style of Text("28 October 2014"), you must change the style of Text("Objective-C,[7] Rust, Haskell, Ruby, Python, C#, CLU,[8] D[9]") too. 83 | ```swift 84 | // in html5.swift 85 | HStack() { 86 | Text("Initial release:") 87 | .font(Font.system(size: 14)) 88 | 89 | Text("28 October 2014") 90 | .font(Font.system(size: 12)) 91 | .foregroundColor(NormalDescColor) 92 | 93 | } 94 | // in swift.swift 95 | HStack(alignment: .top) { 96 | Text("Influenced by:") 97 | .font(Font.system(size: 14)) 98 | 99 | Text("Objective-C,[7] Rust, Haskell, Ruby, Python, C#, CLU,[8] D[9]") 100 | .font(Font.system(size: 12)) 101 | .foregroundColor(NormalDescColor) 102 | 103 | } 104 | ``` 105 | ### With SwiftUI-CSS 106 | You can change the definition of wikiDesc_clsName once for all. 107 | ``` swift 108 | let wikiDesc_clsName = CSSStyle([ 109 | .font(Font.system(size: 12)), 110 | .foregroundColor(NormalDescColor) 111 | ]) 112 | 113 | // in html5.swift 114 | HStack() { 115 | Text("Initial release:") 116 | .font(Font.system(size: 14)) 117 | 118 | Text("28 October 2014") 119 | .addClassName(wikiDesc_clsName) 120 | 121 | } 122 | // in swift.swift 123 | HStack(alignment: .top) { 124 | Text("Influenced by:") 125 | .font(Font.system(size: 14)) 126 | 127 | Text("Objective-C,[7] Rust, Haskell, Ruby, Python, C#, CLU,[8] D[9]") 128 | .addClassName(wikiDesc_clsName) 129 | 130 | } 131 | ``` 132 | 133 | ## the other benefits of using SwiftUI-CSS 134 | 1. more easy to change a lot of styles when state change. 135 | ``` swift 136 | // without swiftui-css 137 | if festival == 'Christmas' { 138 | Text("Welcome everyone!") 139 | .font(.largeTitle) 140 | .foreground(.white) 141 | .background(.red) 142 | } else { 143 | Text("Welcome everyone!") 144 | .font(.title) 145 | .foreground(.darkGray) 146 | .background(.white) 147 | } 148 | 149 | // with 150 | Text("Welcome everyone!") 151 | .addClassName(fesitval == 'Christmas' ? chrismas_clsName: normal_clsName) 152 | ``` 153 | 2. Maybe a reachable way to convert html+css codes to swiftui source 154 | 3. write less code, clear to tell parameters meanings. For example. 155 | > `.frame(minHeight: 50, maxheight: .infinity` to `.flexHeight(min: 50, max: .infinity)` 156 | > `.padding(EdgeInset(top:10, leading: 15, bottom:0, trailing: 20)` to `.paddingTLBT(10,15,0,20)` 157 | 4. You can combile some different style into one. 158 | ```swift 159 | let fontStyle = CSSStyle([.font(.caption)]) 160 | let colorStyle = CSSStyle([.backgroundColor(.red)]) 161 | 162 | let finalStyle = fontStyle + colorStyle 163 | print("finalStyle = \(finalStyle)") 164 | ``` 165 | 5. use responsive class to make view larger on larger screen 166 | ```swift 167 | // In iOS, if the sketch file designed for screen 375x667, the responsive fator should be compared to UIScreen.main.bounds.size.width. 168 | let responsive = Responsive(UIScreen.main.bounds.size.width / 375) 169 | let wikiDesc_clsName = CSSStyle([ 170 | .font(Font.system(size: responsive.r(12))), 171 | .foregroundColor(NormalDescColor) 172 | .paddingEdges([.bottom], responsive.r(10)) 173 | ]) 174 | ``` 175 | 176 | 177 | -------------------------------------------------------------------------------- /Sources/SwiftUI_CSS/Responsive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by hite on 2020/11/3. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | public class Responsive: ObservableObject { 13 | /// For example. the size of view in sketch file designed for iPhone 6 should larger on iPhone by 1.104 ( 414 / 375) times. 14 | /// so the factor = 1.104 15 | @Published public var factor: CGFloat 16 | 17 | public init(_ factor: CGFloat) { 18 | self.factor = factor 19 | } 20 | public convenience init(designSize: CGRect) { 21 | var factor: CGFloat = 1 22 | if designSize.equalTo(.zero) { 23 | factor = 1 24 | } else { 25 | #if os(iOS) 26 | // Code specific to iOS 27 | let size = UIScreen.main.bounds.size 28 | factor = size.width / designSize.width 29 | #elseif os(macOS) 30 | // Code specific to macOS 31 | factor = 1// maybe 1 is not right number. 32 | #else 33 | factor = 1 34 | #endif 35 | } 36 | 37 | self.init(factor) 38 | } 39 | 40 | public func r(_ standardSize: CGFloat) -> CGFloat { 41 | return self.factor * standardSize 42 | } 43 | 44 | public func debug() -> String { 45 | return "" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftUI_CSS/SwiftUI_CSS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSSStyle.swift 3 | // SwiftUI_CSS 4 | // 5 | // Created by liang on 2019/8/6. 6 | // Copyright © 2019 liang. All rights reserved. 7 | // 8 | 9 | 10 | import SwiftUI 11 | import Foundation 12 | 13 | // https://mecid.github.io/2019/08/07/viewmodifiers-in-swiftui/ 14 | extension View { 15 | func eraseToAnyView() -> AnyView { 16 | return AnyView(self) 17 | } 18 | } 19 | 20 | public enum CSSProperty { 21 | case font(Font) 22 | case foregroundColor(Color) 23 | case backgroundColor(Color) 24 | case lineLimit(Int) 25 | case lineSpacing(CGFloat) 26 | 27 | case padding(CGFloat) 28 | case paddingHorizontal(CGFloat) 29 | case paddingVertical(CGFloat) 30 | case paddingEdges(Edge.Set, CGFloat) 31 | // TLBT = top + leading + bottom + trailing .It differs from css-style definition that is top, right, bottom, left 32 | case paddingTLBT(CGFloat, CGFloat, CGFloat, CGFloat) 33 | 34 | case opacity(Double) 35 | case shadow(color: Color, radius: CGFloat, x: CGFloat, y: CGFloat) 36 | case border(color: Color, width: CGFloat) 37 | case cornerRadius(_ radius: CGFloat) 38 | // frame cluster 39 | case frameAlignment(Alignment) 40 | case width(CGFloat) 41 | case height(CGFloat) 42 | case frame(width: CGFloat?, height: CGFloat?, alignment: Alignment) 43 | case flexWidth(min: CGFloat?, max: CGFloat?) 44 | case flexHeight(min: CGFloat?, max: CGFloat?) 45 | 46 | case position(x: CGFloat, y: CGFloat) 47 | case offset(x: CGFloat, y: CGFloat) 48 | case clipped(antialiased: Bool) 49 | 50 | } 51 | 52 | extension CSSProperty { 53 | 54 | } 55 | 56 | public struct CSSStyle { 57 | fileprivate var properties: [CSSProperty] 58 | 59 | public init(_ properties: CSSProperty...) { 60 | self.properties = properties 61 | } 62 | 63 | public init(_ properties: [CSSProperty]) { 64 | self.properties = properties 65 | } 66 | 67 | public init(_ classes: [CSSStyle]) { 68 | self.properties = [] 69 | for classItem in classes { 70 | self.properties += classItem.properties 71 | } 72 | } 73 | 74 | func applyStyle(content: AnyView) -> AnyView { 75 | return properties.reduce(content) { (newContent: AnyView, style: CSSProperty) -> AnyView in 76 | switch style { 77 | 78 | case .font(let font): 79 | return self.applyFont(content: newContent, font: font) 80 | case .foregroundColor(let color): 81 | return self.applyForegroundColor(content: newContent, color: color) 82 | case .backgroundColor(let bgColor): 83 | return self.applyBackgroundColor(content: newContent, color: bgColor) 84 | case .lineLimit(let limit): 85 | return self.applyLineLimit(content: newContent, limit: limit) 86 | case .lineSpacing(let spacing): 87 | return self.applyLineSpacing(content: newContent, spacing: spacing) 88 | case .padding(let padding): 89 | return self.applyPaddingByEdge(content: newContent, directions: [.all], padding: padding) 90 | case .paddingHorizontal(let padding): 91 | return self.applyPaddingByEdge(content: newContent, directions: [.horizontal], padding: padding) 92 | case let .paddingEdges(edges, padding): 93 | return self.applyPaddingByEdge(content: newContent, directions: edges, padding: padding) 94 | case .paddingVertical(let padding): 95 | return self.applyPaddingByEdge(content: newContent, directions: [.vertical], padding: padding) 96 | case let .paddingTLBT(top, leading, bottom, trailing): 97 | return self.applyPaddingAll(content: newContent, inset: .init(top: top, leading: leading, bottom: bottom, trailing: trailing)) 98 | case .opacity(let opacity): 99 | return self.applyOpacity(content: newContent, opacity: opacity) 100 | case let .shadow(color, radius, x, y): 101 | return self.applyShadow(content: newContent, color: color, radius: radius, x: x, y: y) 102 | case let .border(color, width): 103 | return self.applyBorder(content: newContent, color: color, width: width) 104 | case let .cornerRadius(width): 105 | return self.applyCornerRadius(content: newContent, radius: width) 106 | case .frameAlignment(let align): 107 | return self.applyFrameAlignment(content: newContent, align: align) 108 | case .width(let width): 109 | return self.applyWidth(content: newContent, width: width) 110 | case .height(let height): 111 | return self.applyHeight(content: newContent, height: height) 112 | case let .frame(width, height, alignment): 113 | return self.applyFrame(content: newContent, width: width, height: height, alignment: alignment) 114 | case let .flexWidth(minWidth, maxWidth):// the disadv 115 | return self.applyFlexWidth(content: newContent, minWidth: minWidth, idealWidth: nil, maxWidth: maxWidth) 116 | case let .flexHeight(minHeight, maxHeight): 117 | return self.applyFlexHeight(content: newContent, minHeight: minHeight, idealHeight: nil, maxHeight: maxHeight) 118 | case let .position(x, y): 119 | return self.applyPosition(content: newContent, x: x, y: y) 120 | case let .offset(x, y): 121 | return self.applyOffset(content: newContent, x: x, y: y) 122 | case .clipped(let antialiased): 123 | return self.applyClipped(content: newContent, antialiased: antialiased) 124 | } 125 | } 126 | } 127 | 128 | func applyForegroundColor(content: AnyView, color: Color) -> AnyView { 129 | return content.foregroundColor(color).eraseToAnyView() 130 | } 131 | func applyFont(content: AnyView, font: Font?) -> AnyView { 132 | return content.font(font).eraseToAnyView() 133 | } 134 | func applyBackgroundColor(content: AnyView, color: Color) -> AnyView { 135 | return content.background(color).eraseToAnyView() 136 | } 137 | func applyLineLimit(content: AnyView, limit: Int) -> AnyView { 138 | return content.lineLimit(limit).eraseToAnyView() 139 | } 140 | func applyLineSpacing(content: AnyView, spacing: CGFloat) -> AnyView { 141 | return content.lineSpacing(spacing).eraseToAnyView() 142 | } 143 | func applyPaddingByEdge(content: AnyView, directions: Edge.Set, padding: CGFloat) -> AnyView { 144 | return content.padding(directions, padding).eraseToAnyView() 145 | } 146 | func applyPaddingAll(content: AnyView, inset: EdgeInsets) -> AnyView { 147 | return content.padding(inset).eraseToAnyView() 148 | } 149 | func applyOpacity(content: AnyView, opacity: Double) -> AnyView { 150 | return content.opacity(opacity).eraseToAnyView() 151 | } 152 | func applyShadow(content: AnyView, color: Color, radius: CGFloat, x: CGFloat, y: CGFloat) -> AnyView { 153 | return content.shadow(color: color, radius: radius, x: x, y: y).eraseToAnyView() 154 | } 155 | func applyBorder(content: AnyView, color: Color, width: CGFloat) -> AnyView { 156 | return content.border(color, width: width).eraseToAnyView() 157 | } 158 | func applyCornerRadius(content: AnyView, radius: CGFloat) -> AnyView { 159 | return content.cornerRadius(radius, antialiased: true).eraseToAnyView() 160 | } 161 | func applyFrameAlignment(content: AnyView, align: Alignment) -> AnyView { 162 | return content.frame(alignment: align).eraseToAnyView() 163 | } 164 | func applyWidth(content: AnyView, width: CGFloat) -> AnyView { 165 | return content.frame(width: width).eraseToAnyView() 166 | } 167 | func applyHeight(content: AnyView, height: CGFloat) -> AnyView { 168 | return content.frame(height: height).eraseToAnyView() 169 | } 170 | func applyFrame(content: AnyView, width: CGFloat?, height: CGFloat?, alignment: Alignment) -> AnyView { 171 | return content.frame(width: width, height: height, alignment: alignment).eraseToAnyView() 172 | } 173 | func applyFlexWidth(content: AnyView, minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?) -> AnyView { 174 | return content.frame(minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth).eraseToAnyView() 175 | } 176 | func applyFlexHeight(content: AnyView, minHeight: CGFloat?, idealHeight: CGFloat?, maxHeight: CGFloat?) -> AnyView { 177 | return content.frame(minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight).eraseToAnyView() 178 | } 179 | func applyPosition(content: AnyView, x: CGFloat, y: CGFloat) -> AnyView { 180 | return content.position(x:x, y: y).eraseToAnyView() 181 | } 182 | func applyOffset(content: AnyView, x: CGFloat, y: CGFloat) -> AnyView { 183 | return content.offset(x:x, y: y).eraseToAnyView() 184 | } 185 | func applyClipped(content: AnyView, antialiased: Bool) -> AnyView { 186 | return content.clipped(antialiased: antialiased).eraseToAnyView() 187 | } 188 | 189 | } 190 | 191 | extension CSSStyle { 192 | public static func +(left: CSSStyle, right: CSSStyle) -> CSSStyle{ 193 | return CSSStyle(left.properties + right.properties) 194 | } 195 | } 196 | 197 | struct CSSStyleModifier: ViewModifier { 198 | fileprivate let styleSheet: CSSStyle 199 | 200 | func body(content: _ViewModifier_Content) -> some View { 201 | 202 | return styleSheet.applyStyle(content: content.eraseToAnyView()) 203 | } 204 | } 205 | 206 | 207 | extension View { 208 | public func addClassName(_ clsName: CSSStyle...) -> some View { 209 | ModifiedContent(content: self, modifier: CSSStyleModifier(styleSheet: CSSStyle(clsName))) 210 | } 211 | 212 | public func setStyle(_ properties: CSSProperty...) -> some View { 213 | ModifiedContent(content: self, modifier: CSSStyleModifier(styleSheet: CSSStyle(properties))) 214 | } 215 | 216 | func changeClassName(_ clsName: CSSStyle) -> some View { 217 | // 218 | return self 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftUI_CSSTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftUI_CSSTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftUI_CSSTests/SwiftUI_CSSTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftUI_CSS 3 | 4 | final class SwiftUI_CSSTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | let a = CSSStyle([.font(.caption)]) 10 | let b = CSSStyle([.backgroundColor(.red)]) 11 | 12 | let c = a + b 13 | print("a+b = \(c)") 14 | } 15 | 16 | static var allTests = [ 17 | ("testExample", testExample), 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Tests/SwiftUI_CSSTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftUI_CSSTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------