Skip to content

Commit

Permalink
Merge pull request #10 from modestman/feature/export-colors-as-code
Browse files Browse the repository at this point in the history
Export colors as swift code without assets
  • Loading branch information
subdan authored May 28, 2020
2 parents 189e456 + f7a91de commit 33244eb
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ let package = Package(
.testTarget(
name: "figma-exportTests",
dependencies: ["FigmaExport"]
),
.testTarget(
name: "XcodeExportTests",
dependencies: ["XcodeExport"]
)
]
)
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ Additionally the `Color.swift` file will be created to use colors from the code.

```

If you set option `useColorAssets: False` in the configuration file, then will be generated code like this:
```swift
import UIKit

extension UIColor {
static var primaryText: UIColor {
UIColor { traitCollection -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
return UIColor(red: 0.000, green: 0.000, blue: 0.000, alpha: 1.000)
} else {
return UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
}
}
}
static var backgroundVideo: UIColor {
return UIColor(red: 0.467, green: 0.012, blue: 1.000, alpha: 0.500)
}
}
```

#### Icons

Icons will be exported as PDF files with `Template Image` render mode.
Expand Down Expand Up @@ -156,6 +176,8 @@ ios:

# Parameters for exporting colors
colors:
# Should be generate color assets instead of pure swift code
useColorAssets: True
# Name of the folder inside Assets.xcassets where to place colors (.colorset directories)
assetsFolder: Colors
# Path to Color.swift file where to export colors for accessing colors from the code (e.g. UIColor.backgroundPrimary)
Expand Down Expand Up @@ -194,7 +216,8 @@ android:

### iOS properties
* `ios.xcassetsPath` — Relative or absolute path to directory `Assets.xcassets` where to export colors, icons and images.
* `ios.colors.assetsFolder` — Name of the folder inside `Assets.xcassets` where colors will be exported.
* `ios.colors.useColorAssets` — How to export colors - as assets or as swift UIColor initializers only.
* `ios.colors.assetsFolder` — Name of the folder inside `Assets.xcassets` where colors will be exported. Used only if `useColorAssets == true`.
* `ios.colors.colorSwift` — Relative or absolute path to `Color.swift` file.
* `ios.colors.nameStyle` — Color name style: camelCase or snake_case
* `ios.icons.assetsFolder` — Name of the folder inside `Assets.xcassets` where icons will be exported.
Expand Down Expand Up @@ -245,4 +268,4 @@ If you have any issues with the FigmaExport or you want some new features feel f

## Authors

Daniil Subbotin - [email protected]
Daniil Subbotin - [email protected]
2 changes: 2 additions & 0 deletions Release/figma-export.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ ios:

# Parameters for exporting colors
colors:
# Should be generate color assets instead of pure swift code
useColorAssets: True
# Name of the folder inside Assets.xcassets where to place colors (.colorset directories)
assetsFolder: Colors
# Absolute path to Color.swift file where to export colors for accessing colors from the code (e.g. UIColor.backgroundPrimary)
Expand Down
3 changes: 2 additions & 1 deletion Sources/FigmaExport/Input/Params.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ struct Params: Decodable {
struct iOS: Decodable {

struct Colors: Decodable {
let assetsFolder: String
let useColorAssets: Bool
let assetsFolder: String?
let colorSwift: URL
let nameStyle: NameStyle
}
Expand Down
9 changes: 8 additions & 1 deletion Sources/FigmaExport/Subcommands/ExportColors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ extension FigmaExportCommand {
}

private func exportXcodeColors(colorPairs: [AssetPair<Color>], iosParams: Params.iOS) throws {
let colorsURL = iosParams.xcassetsPath.appendingPathComponent(iosParams.colors.assetsFolder)
var colorsURL: URL? = nil
if iosParams.colors.useColorAssets {
if let folder = iosParams.colors.assetsFolder {
colorsURL = iosParams.xcassetsPath.appendingPathComponent(folder)
} else {
throw FigmaExportError.colorsAssetsFolderNotSpecified
}
}

let output = XcodeColorsOutput(
assetsColorsURL: colorsURL,
Expand Down
3 changes: 3 additions & 0 deletions Sources/FigmaExport/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import Foundation
enum FigmaExportError: LocalizedError {

case accessTokenNotFound
case colorsAssetsFolderNotSpecified

var errorDescription: String? {
switch self {
case .accessTokenNotFound:
return "Environment varibale FIGMA_PERSONAL_TOKEN not specified."
case .colorsAssetsFolderNotSpecified:
return "Option ios.colors.assetsFolder not specified in configuration file."
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/XcodeExport/Model/XcodeColorsOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Foundation

public struct XcodeColorsOutput {

public let assetsColorsURL: URL
public let assetsColorsURL: URL?
public let colorSwiftURL: URL

public init(assetsColorsURL: URL, colorSwiftURL: URL) {
public init(assetsColorsURL: URL?, colorSwiftURL: URL) {
self.assetsColorsURL = assetsColorsURL
self.colorSwiftURL = colorSwiftURL
}
Expand Down
49 changes: 41 additions & 8 deletions Sources/XcodeExport/XcodeColorExporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final public class XcodeColorExporter {
var files: [FileContents] = []

// Sources/.../Color.swift
let contents = prepareColorDotSwiftContents(colorPairs)
let contents = prepareColorDotSwiftContents(colorPairs, formAsset: output.assetsColorsURL != nil)
let contentsData = contents.data(using: .utf8)!

let fileURL = URL(string: output.colorSwiftURL.lastPathComponent)!
Expand All @@ -26,31 +26,33 @@ final public class XcodeColorExporter {
)
)

guard let assetsColorsURL = output.assetsColorsURL else { return files }

// Assets.xcassets/Colors/Contents.json
let contentsJson = XcodeEmptyContents()
files.append(FileContents(
destination: Destination(directory: output.assetsColorsURL, file: contentsJson.fileURL),
destination: Destination(directory: assetsColorsURL, file: contentsJson.fileURL),
data: contentsJson.data
))

// Assets.xcassets/Colors/***.colorset/Contents.json
colorPairs.forEach { colorPair in
let name = colorPair.light.name
let dirURL = output.assetsColorsURL.appendingPathComponent("\(name).colorset")
let dirURL = assetsColorsURL.appendingPathComponent("\(name).colorset")

var colors: [XcodeAssetContents.ColorData] = [
XcodeAssetContents.ColorData(
appearances: nil,
color: XcodeAssetContents.ColorInfo(
components: colorPair.light.toComponents())
components: colorPair.light.toHexComponents())
)
]
if let darkColor = colorPair.dark {
colors.append(
XcodeAssetContents.ColorData(
appearances: [XcodeAssetContents.DarkAppeareance()],
color: XcodeAssetContents.ColorInfo(
components: darkColor.toComponents())
components: darkColor.toHexComponents())
)
)
}
Expand All @@ -69,7 +71,7 @@ final public class XcodeColorExporter {
return files
}

private func prepareColorDotSwiftContents(_ colorPairs: [AssetPair<Color>]) -> String {
private func prepareColorDotSwiftContents(_ colorPairs: [AssetPair<Color>], formAsset: Bool) -> String {
var contents = """
import UIKit
Expand All @@ -78,7 +80,30 @@ final public class XcodeColorExporter {
"""

colorPairs.forEach { colorPair in
contents.append(" static var \(colorPair.light.name): UIColor { return UIColor(named: #function)! }\n")
if formAsset {
contents.append(" static var \(colorPair.light.name): UIColor { return UIColor(named: #function)! }\n")
} else {
let lightComponents = colorPair.light.toRgbComponents()
if let darkComponents = colorPair.dark?.toRgbComponents() {
contents.append("""
static var \(colorPair.light.name): UIColor {
UIColor { traitCollection -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
return UIColor(red: \(darkComponents.red), green: \(darkComponents.green), blue: \(darkComponents.blue), alpha: \(darkComponents.alpha))
} else {
return UIColor(red: \(lightComponents.red), green: \(lightComponents.green), blue: \(lightComponents.blue), alpha: \(lightComponents.alpha))
}
}
}\n
""")
} else {
contents.append("""
static var \(colorPair.light.name): UIColor {
return UIColor(red: \(lightComponents.red), green: \(lightComponents.green), blue: \(lightComponents.blue), alpha: \(lightComponents.alpha))
}\n
""")
}
}
}
contents.append("\n}\n")

Expand All @@ -87,7 +112,7 @@ final public class XcodeColorExporter {
}

private extension Color {
func toComponents() -> XcodeAssetContents.Components {
func toHexComponents() -> XcodeAssetContents.Components {
let red = "0x\(doubleToHex(self.red))"
let green = "0x\(doubleToHex(self.green))"
let blue = "0x\(doubleToHex(self.blue))"
Expand All @@ -98,4 +123,12 @@ private extension Color {
func doubleToHex(_ double: Double) -> String {
String(format: "%02X", arguments: [Int((double * 255).rounded())])
}

func toRgbComponents() -> XcodeAssetContents.Components {
let red = String(format: "%.3F", arguments: [self.red])
let green = String(format: "%.3F", arguments: [self.green])
let blue = String(format: "%.3F", arguments: [self.blue])
let alpha = String(format: "%.3F", arguments: [self.alpha])
return XcodeAssetContents.Components(red: red, alpha: alpha, green: green, blue: blue)
}
}
93 changes: 93 additions & 0 deletions Tests/XcodeExportTests/XcodeColorExporterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import XCTest
import FigmaExportCore
@testable import XcodeExport

final class XcodeColorExporterTests: XCTestCase {

// MARK: - Properties

private let fileManager = FileManager.default
private var colorsFile: URL!
private var colorsAsssetCatalog: URL!

private let colorPair1 = AssetPair<Color>(
light: Color(name: "colorPair1", r: 1, g: 1, b: 1, a: 1),
dark: Color(name: "colorPair1", r: 0, g: 0, b: 0, a: 1))

private let colorPair2 = AssetPair<Color>(
light: Color(name: "colorPair2", r: 119.0/255.0, g: 3.0/255.0, b: 1.0, a: 0.5),
dark: nil)

// MARK: - Setup

override func setUp() {
super.setUp()
colorsFile = fileManager.temporaryDirectory.appendingPathComponent("Colors.swift")
colorsAsssetCatalog = fileManager.temporaryDirectory.appendingPathComponent("Assets.xcassets/Colors")
}

// MARK: - Tests

func testExport_without_assets() {
let output = XcodeColorsOutput(assetsColorsURL: nil, colorSwiftURL: colorsFile)
let exporter = XcodeColorExporter(output: output)

let result = exporter.export(colorPairs: [colorPair1, colorPair2])
XCTAssertEqual(result.count, 1)

let content = result[0].data
XCTAssertNotNil(content)

let generatedCode = String(data: content!, encoding: .utf8)
let referenceCode = """
import UIKit
extension UIColor {
static var colorPair1: UIColor {
UIColor { traitCollection -> UIColor in
if traitCollection.userInterfaceStyle == .dark {
return UIColor(red: 0.000, green: 0.000, blue: 0.000, alpha: 1.000)
} else {
return UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
}
}
}
static var colorPair2: UIColor {
return UIColor(red: 0.467, green: 0.012, blue: 1.000, alpha: 0.500)
}
}
"""
XCTAssertEqual(generatedCode, referenceCode)
}

func testExport_with_assets() {
let output = XcodeColorsOutput(assetsColorsURL: colorsAsssetCatalog, colorSwiftURL: colorsFile)
let exporter = XcodeColorExporter(output: output)
let result = exporter.export(colorPairs: [colorPair1, colorPair2])

XCTAssertEqual(result.count, 4)
XCTAssertTrue(result[0].destination.url.absoluteString.hasSuffix("Colors.swift"))
XCTAssertTrue(result[1].destination.url.absoluteString.hasSuffix("Assets.xcassets/Colors/Contents.json"))
XCTAssertTrue(result[2].destination.url.absoluteString.hasSuffix("colorPair1.colorset/Contents.json"))
XCTAssertTrue(result[3].destination.url.absoluteString.hasSuffix("colorPair2.colorset/Contents.json"))

let content = result[0].data
XCTAssertNotNil(content)

let generatedCode = String(data: content!, encoding: .utf8)
let referenceCode = """
import UIKit
extension UIColor {
static var colorPair1: UIColor { return UIColor(named: #function)! }
static var colorPair2: UIColor { return UIColor(named: #function)! }
}
"""
XCTAssertEqual(generatedCode, referenceCode)
}

}
2 changes: 1 addition & 1 deletion Tests/figma-exportTests/figma_exportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class figma_exportTests: XCTestCase {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)

XCTAssertEqual(output, "Hello, world!\n")
XCTAssertEqual(output, "")
}

/// Returns path to the built products directory.
Expand Down

0 comments on commit 33244eb

Please sign in to comment.