From 8adc6020723f9ffce3c67341305e9b8e9376b963 Mon Sep 17 00:00:00 2001
From: Ilia <>
Date: Wed, 15 Jun 2022 16:45:40 +0200
Subject: [PATCH] Added initial version of spacing grid export for review by
RMR
---
CONFIG.md | 33 ++++++
.../ui/figmaexport/Spacings.kt | 73 ++++++++++++
.../app/src/main/res/values/dimens.xml | 20 +++-
.../UIComponents/Source/Spacings.swift | 30 +++++
README.md | 47 ++++++++
.../AndroidSpacingsExporter.swift | 78 +++++++++++++
.../Resources/Spacings.kt.stencil | 14 +++
.../Resources/spacings.xml.stencil | 4 +
.../Endpoint/ComponentsEndpoint.swift | 7 ++
Sources/FigmaExport/Input/Params.swift | 30 +++++
.../FigmaExport/Loaders/SpacingsLoader.swift | 86 ++++++++++++++
.../Subcommands/ExportSpacings.swift | 110 ++++++++++++++++++
Sources/FigmaExport/main.swift | 1 +
.../Processor/AssetsProcessor.swift | 16 +++
Sources/FigmaExportCore/Spacing.swift | 29 +++++
Sources/FigmaExportCore/TextStyle.swift | 5 +-
.../Model/XcodeSpacingsOutput.swift | 17 +++
.../Resources/Spacings.swift.stencil | 6 +
.../XcodeExport/XcodeSpacingsExporter.swift | 25 ++++
19 files changed, 626 insertions(+), 5 deletions(-)
create mode 100644 Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt
create mode 100644 Examples/Example/UIComponents/Source/Spacings.swift
create mode 100644 Sources/AndroidExport/AndroidSpacingsExporter.swift
create mode 100644 Sources/AndroidExport/Resources/Spacings.kt.stencil
create mode 100644 Sources/AndroidExport/Resources/spacings.xml.stencil
create mode 100644 Sources/FigmaExport/Loaders/SpacingsLoader.swift
create mode 100644 Sources/FigmaExport/Subcommands/ExportSpacings.swift
create mode 100644 Sources/FigmaExportCore/Spacing.swift
create mode 100644 Sources/XcodeExport/Model/XcodeSpacingsOutput.swift
create mode 100644 Sources/XcodeExport/Resources/Spacings.swift.stencil
create mode 100644 Sources/XcodeExport/XcodeSpacingsExporter.swift
diff --git a/CONFIG.md b/CONFIG.md
index d202c641..94589018 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -38,6 +38,7 @@ common:
lightHCModeSuffix: '_lightHC'
# [optional] If useSingleFile is true, customize the suffix to denote a dark high contrast color. Defaults to '_darkHC'
darkHCModeSuffix: '_darkHC'
+
# [optional]
icons:
# [optional] Name of the Figma's frame where icons components are located
@@ -50,6 +51,7 @@ common:
useSingleFile: true
# [optional] If useSingleFile is true, customize the suffix to denote a dark mode icons. Defaults to '_dark'
darkModeSuffix: '_dark'
+
# [optional]
images:
# [optional]Name of the Figma's frame where image components are located
@@ -62,12 +64,26 @@ common:
useSingleFile: true
# [optional] If useSingleFile is true, customize the suffix to denote a dark mode icons. Defaults to '_dark'
darkModeSuffix: '_dark'
+
# [optional]
typography:
# [optional] RegExp pattern for text style name validation before exporting. If a name contains "/" symbol it will be replaced by "_" before executing the RegExp
nameValidateRegexp: '^[a-zA-Z0-9_]+$' # RegExp pattern for: h1_regular, h1_medium
# [optional] RegExp pattern for replacing. Supports only $n
nameReplaceRegexp: 'font_$1'
+
+ # [optional]
+ spacings:
+ # [optional] Frame name containing spacings
+ figmaFrameName: 'Spacings'
+ # [optional] Containing state name for vertical spacings
+ figmaVerticalStateName: 'Vertical'
+ # [optional] Containing state name for horizontal spacings
+ figmaHorizontalStateName: 'Horizontal'
+ # [optional] RegExp pattern for text style name validation before exporting. If a name contains "/" symbol it will be replaced by "_" before executing the RegExp
+ nameValidateRegexp: '^[a-zA-Z0-9_]+$' # RegExp pattern for: h1_regular, h1_medium
+ # [optional] RegExp pattern for replacing. Supports only $n
+ nameReplaceRegexp: 'font_$1'
# [optional] iOS export parameters
ios:
@@ -157,6 +173,13 @@ ios:
labelsDirectory: "./Source/UIComponents/"
# Typography name style: camelCase or snake_case
nameStyle: camelCase
+
+ # [optional] Parameters for exporting spacings
+ spacings:
+ # Relative or absolute path to directory where to place generated Spacings.swift file
+ spacingsSwift: "./Source/UIComponents/"
+ # Spacing name style: camelCase or snake_case
+ nameStyle: camelCase
# [optional] Android export parameters
android:
@@ -173,12 +196,14 @@ android:
colors:
# [optional] The package to export the Jetpack Compose color code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set
composePackageName: "com.example"
+
# Parameters for exporting icons
icons:
# Where to place icons relative to `mainRes`? FigmaExport clears this directory every time your execute `figma-export icons` command
output: "figma-import-icons"
# [optional] The package to export the Jetpack Compose icon code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set
composePackageName: "com.example"
+
# Parameters for exporting images
images:
# Image file format: svg or png
@@ -193,10 +218,18 @@ android:
encoding: lossy
# Encoding quality in percents. Only for lossy encoding.
quality: 90
+
# Parameters for exporting typography
typography:
# Typography name style: camelCase or snake_case
nameStyle: camelCase
# [optional] The package to export the Jetpack Compose typography code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set
composePackageName: "com.example"
+
+ # Parameters for exporting spacings
+ spacings:
+ # Spacings name style: camelCase or snake_case
+ nameStyle: camelCase
+ # [optional] The package to export the Jetpack Compose typography code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set
+ composePackageName: "com.example"
```
diff --git a/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt b/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt
new file mode 100644
index 00000000..fc4ff780
--- /dev/null
+++ b/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt
@@ -0,0 +1,73 @@
+package com.redmadrobot.androidcomposeexample.ui.figmaexport
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import com.redmadrobot.androidcomposeexample.R
+
+object Spacings
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h12(): Dp = dimensionResource(id = R.dimen.h12)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h16(): Dp = dimensionResource(id = R.dimen.h16)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h24(): Dp = dimensionResource(id = R.dimen.h24)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h32(): Dp = dimensionResource(id = R.dimen.h32)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h4(): Dp = dimensionResource(id = R.dimen.h4)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h48(): Dp = dimensionResource(id = R.dimen.h48)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h64(): Dp = dimensionResource(id = R.dimen.h64)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h8(): Dp = dimensionResource(id = R.dimen.h8)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v12(): Dp = dimensionResource(id = R.dimen.v12)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v16(): Dp = dimensionResource(id = R.dimen.v16)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v24(): Dp = dimensionResource(id = R.dimen.v24)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v32(): Dp = dimensionResource(id = R.dimen.v32)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v4(): Dp = dimensionResource(id = R.dimen.v4)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v48(): Dp = dimensionResource(id = R.dimen.v48)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v64(): Dp = dimensionResource(id = R.dimen.v64)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.v8(): Dp = dimensionResource(id = R.dimen.v8)
diff --git a/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml b/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml
index e00c2dd1..4d0381d7 100644
--- a/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml
+++ b/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml
@@ -1,5 +1,19 @@
+
-
- 16dp
- 16dp
+ 12.0dp
+ 16.0dp
+ 24.0dp
+ 32.0dp
+ 4.0dp
+ 48.0dp
+ 64.0dp
+ 8.0dp
+ 12.0dp
+ 16.0dp
+ 24.0dp
+ 32.0dp
+ 4.0dp
+ 48.0dp
+ 64.0dp
+ 8.0dp
\ No newline at end of file
diff --git a/Examples/Example/UIComponents/Source/Spacings.swift b/Examples/Example/UIComponents/Source/Spacings.swift
new file mode 100644
index 00000000..d21ebb29
--- /dev/null
+++ b/Examples/Example/UIComponents/Source/Spacings.swift
@@ -0,0 +1,30 @@
+// swiftlint:disable all
+//
+// The code generated using FigmaExport — Command line utility to export
+// colors, typography, icons and images from Figma to Xcode project.
+//
+// https://github.com/RedMadRobot/figma-export
+//
+// Don’t edit this code manually to avoid runtime crashes
+//
+
+import Foundation
+
+public struct Spacings {
+ static let h12: Double = 12.0
+ static let h16: Double = 16.0
+ static let h24: Double = 24.0
+ static let h32: Double = 32.0
+ static let h4: Double = 4.0
+ static let h48: Double = 48.0
+ static let h64: Double = 64.0
+ static let h8: Double = 8.0
+ static let v12: Double = 12.0
+ static let v16: Double = 16.0
+ static let v24: Double = 24.0
+ static let v32: Double = 32.0
+ static let v4: Double = 4.0
+ static let v48: Double = 48.0
+ static let v64: Double = 64.0
+ static let v8: Double = 8.0
+}
diff --git a/README.md b/README.md
index d85dc3f7..d9a6c61e 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ Command line utility to export colors, typography, icons and images from Figma t
* typography - Figma's text style
* icon — Figma's component with small black/colorized vector image
* image — Figma's components with colorized image (Light/Dark)
+* spacings — Figma's components defining spacing between other components
The utility supports Dark Mode, SwiftUI and Jetpack Compose.
@@ -199,6 +200,31 @@ Example of these files:
- [./Examples/Example/UIComponents/Source/LabelStyle.swift](./Examples/Example/UIComponents/Source/LabelStyle.swift)
- [./Examples/Example/UIComponents/Source/UIFont+extension.swift](./Examples/Example/UIComponents/Source/UIFont+extension.swift)
+#### Spacings
+When your execute `figma-export spacings` command `figma-export` generates a Spacings.swift that looks like this:
+```swift
+import Foundation
+
+public struct Spacings {
+ static let h12: Double = 12.0
+ static let h16: Double = 16.0
+ static let h24: Double = 24.0
+ static let h32: Double = 32.0
+ static let h4: Double = 4.0
+ static let h48: Double = 48.0
+ static let h64: Double = 64.0
+ static let h8: Double = 8.0
+ static let v12: Double = 12.0
+ static let v16: Double = 16.0
+ static let v24: Double = 24.0
+ static let v32: Double = 32.0
+ static let v4: Double = 4.0
+ static let v48: Double = 48.0
+ static let v64: Double = 64.0
+ static let v8: Double = 8.0
+}
+```
+
### Android
Colors will be exported to `values/colors.xml` and `values-night/colors.xml` files.
@@ -258,6 +284,27 @@ object Typography {
}
```
+Spacings will be exported to `values/dimens.xml`. For Jetpack Compose, following code will be generated, if configured:
+```kotlin
+package com.redmadrobot.androidcomposeexample.ui.figmaexport
+
+import ...
+
+object Spacings
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h12(): Dp = dimensionResource(id = R.dimen.h12)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h16(): Dp = dimensionResource(id = R.dimen.h16)
+
+@Composable
+@ReadOnlyComposable
+fun Spacings.h24(): Dp = dimensionResource(id = R.dimen.h24)
+```
+
## Installation
Before installation you must provide Figma personal access token via environment variables.
diff --git a/Sources/AndroidExport/AndroidSpacingsExporter.swift b/Sources/AndroidExport/AndroidSpacingsExporter.swift
new file mode 100644
index 00000000..068afa9a
--- /dev/null
+++ b/Sources/AndroidExport/AndroidSpacingsExporter.swift
@@ -0,0 +1,78 @@
+import Foundation
+import FigmaExportCore
+
+final public class AndroidSpacingsExporter: AndroidExporter {
+
+ private let output: AndroidOutput
+
+ public init(output: AndroidOutput) {
+ self.output = output
+ super.init(templatesPath: output.templatesPath)
+ }
+
+ public func exportSpacings(spacings: [Spacing]) throws -> [FileContents] {
+ var files: [FileContents] = []
+
+ // typography.xml
+ files.append(try makeSpacingsXMLFileContents(spacings: spacings))
+
+ // Typography.kt
+ if
+ let composeOutputDirectory = output.composeOutputDirectory,
+ let packageName = output.packageName,
+ let xmlResourcePackage = output.xmlResourcePackage {
+
+ files.append(
+ try makeSpacingsComposeFileContents(
+ spacings: spacings,
+ outputDirectory: composeOutputDirectory,
+ package: packageName,
+ xmlResourcePackage: xmlResourcePackage
+ )
+ )
+ }
+
+ return files
+ }
+
+ private func makeSpacingsXMLFileContents(spacings: [Spacing]) throws -> FileContents {
+ let env = makeEnvironment()
+ let contents = try env.renderTemplate(name: "spacings.xml.stencil", context: [
+ "spacings": spacings.map { spacing in
+ [
+ "name": spacing.name,
+ "size": spacing.size
+ ]
+ }
+ ])
+
+ let directoryURL = output.xmlOutputDirectory.appendingPathComponent("values")
+ let fileURL = URL(string: "dimens.xml")!
+ return try makeFileContents(for: contents, directory: directoryURL, file: fileURL)
+ }
+
+ private func makeSpacingsComposeFileContents(
+ spacings: [Spacing],
+ outputDirectory: URL,
+ package: String,
+ xmlResourcePackage: String
+ ) throws -> FileContents {
+ let spacings: [[String: Any]] = spacings.map { spacing in
+ [
+ "functionName": spacing.name.lowerCamelCased(),
+ "name": spacing.name
+ ]
+ }
+ let context: [String: Any] = [
+ "spacings": spacings,
+ "package": package,
+ "xmlResourcePackage": xmlResourcePackage
+ ]
+ let env = makeEnvironment()
+ let contents = try env.renderTemplate(name: "Spacings.kt.stencil", context: context)
+
+ let fileURL = URL(string: "Spacings.kt")!
+ return try makeFileContents(for: contents, directory: outputDirectory, file: fileURL)
+ }
+}
+
diff --git a/Sources/AndroidExport/Resources/Spacings.kt.stencil b/Sources/AndroidExport/Resources/Spacings.kt.stencil
new file mode 100644
index 00000000..4a4d9af9
--- /dev/null
+++ b/Sources/AndroidExport/Resources/Spacings.kt.stencil
@@ -0,0 +1,14 @@
+package {{package}}
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import {{xmlResourcePackage}}.R
+
+object Spacings
+{% for spacing in spacings %}
+@Composable
+@ReadOnlyComposable
+fun Spacings.{{ spacing.functionName }}(): Dp = dimensionResource(id = R.dimen.{{ spacing.name }})
+{% endfor %}
\ No newline at end of file
diff --git a/Sources/AndroidExport/Resources/spacings.xml.stencil b/Sources/AndroidExport/Resources/spacings.xml.stencil
new file mode 100644
index 00000000..3d69d4d8
--- /dev/null
+++ b/Sources/AndroidExport/Resources/spacings.xml.stencil
@@ -0,0 +1,4 @@
+
+{% for spacing in spacings %}
+ {{ spacing.size }}dp{% endfor %}
+
\ No newline at end of file
diff --git a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift
index 35fdd41e..a375480b 100644
--- a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift
+++ b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift
@@ -46,4 +46,11 @@ public struct ContainingFrame: Codable {
public let nodeID: String?
public let name: String?
public let pageName: String
+ public let containingStateGroup: ContainingStateGroup?
+}
+
+// MARK: - ContainingStateGroup
+public struct ContainingStateGroup: Codable {
+ public let nodeID: String?
+ public let name: String?
}
diff --git a/Sources/FigmaExport/Input/Params.swift b/Sources/FigmaExport/Input/Params.swift
index 4f8db0a5..e4e27881 100644
--- a/Sources/FigmaExport/Input/Params.swift
+++ b/Sources/FigmaExport/Input/Params.swift
@@ -14,6 +14,7 @@ struct Params: Decodable {
}
struct Common: Decodable {
+
struct Colors: Decodable {
let nameValidateRegexp: String?
let nameReplaceRegexp: String?
@@ -44,10 +45,19 @@ struct Params: Decodable {
let nameReplaceRegexp: String?
}
+ struct Spacings: Decodable {
+ let figmaFrameName: String?
+ let figmaVerticalStateName: String?
+ let figmaHorizontalStateName: String?
+ let nameValidateRegexp: String?
+ let nameReplaceRegexp: String?
+ }
+
let colors: Colors?
let icons: Icons?
let images: Images?
let typography: Typography?
+ let spacings: Spacings?
}
enum VectorFormat: String, Decodable {
@@ -100,6 +110,11 @@ struct Params: Decodable {
let nameStyle: NameStyle
}
+ struct Spacings: Decodable {
+ let spacingsSwift: URL?
+ let nameStyle: NameStyle
+ }
+
let xcodeprojPath: String
let target: String
let xcassetsPath: URL
@@ -112,22 +127,28 @@ struct Params: Decodable {
let icons: Icons?
let images: Images?
let typography: Typography?
+ let spacings: Spacings?
}
struct Android: Decodable {
+
struct Icons: Decodable {
let output: String
let composePackageName: String?
}
+
struct Colors: Decodable {
let composePackageName: String?
}
+
struct Images: Decodable {
+
enum Format: String, Decodable {
case svg
case png
case webp
}
+
struct FormatOptions: Decodable {
enum Encoding: String, Decodable {
case lossy
@@ -136,15 +157,23 @@ struct Params: Decodable {
let encoding: Encoding
let quality: Int?
}
+
let scales: [Double]?
let output: String
let format: Format
let webpOptions: FormatOptions?
}
+
struct Typography: Decodable {
let nameStyle: NameStyle
let composePackageName: String?
}
+
+ struct Spacings: Decodable {
+ let nameStyle: NameStyle
+ let composePackageName: String?
+ }
+
let mainRes: URL
let resourcePackage: String?
let mainSrc: URL?
@@ -152,6 +181,7 @@ struct Params: Decodable {
let icons: Icons?
let images: Images?
let typography: Typography?
+ let spacings: Spacings?
let templatesPath: URL?
}
diff --git a/Sources/FigmaExport/Loaders/SpacingsLoader.swift b/Sources/FigmaExport/Loaders/SpacingsLoader.swift
new file mode 100644
index 00000000..1097ab4d
--- /dev/null
+++ b/Sources/FigmaExport/Loaders/SpacingsLoader.swift
@@ -0,0 +1,86 @@
+import Foundation
+import FigmaAPI
+import FigmaExportCore
+
+/// Loads spacings from Figma
+final class SpacingsLoader {
+
+ private let client: Client
+ private let params: Params
+ private let platform: Platform
+
+ private var spacingsFrameName: String {
+ params.common?.spacings?.figmaFrameName ?? "Spacing"
+ }
+
+ private var spacingsVerticalFrameName: String {
+ params.common?.spacings?.figmaVerticalStateName ?? "Vertical"
+ }
+
+ private var spacingsHorizontalFrameName: String {
+ params.common?.spacings?.figmaHorizontalStateName ?? "Horizontal"
+ }
+
+ init(client: Client, params: Params, platform: Platform) {
+ self.client = client
+ self.params = params
+ self.platform = platform
+ }
+
+ func load() throws -> [Spacing] {
+ return try loadSpacings(fileId: params.figma.lightFileId)
+ }
+
+ private func loadSpacings(fileId: String) throws -> [Spacing] {
+ let formatter = NumberFormatter()
+
+ formatter.minimumFractionDigits = 0
+ formatter.maximumFractionDigits = 1
+
+ return try fetchSpacingsComponents(fileId: fileId).compactMap { component in
+ guard let size = extractSizeFrom(componentName: component.name),
+ let formattedSize = formatter.string(from: NSNumber(value: size)),
+ let containingStateGroup = component.containingFrame.containingStateGroup?.name,
+ let prefix = calculatePrefixFrom(stateGroup: containingStateGroup) else { return nil }
+
+ return Spacing(name: "\(prefix)_\(formattedSize)", size: size)
+ }
+ }
+
+ // MARK: - Helpers
+
+ private func fetchSpacingsComponents(fileId: String) throws -> [Component] {
+ return try loadComponents(fileId: fileId)
+ .filter {
+ $0.containingFrame.name == spacingsFrameName && $0.useForPlatform(platform)
+ }
+ }
+
+ // MARK: - Figma
+ private func loadComponents(fileId: String) throws -> [Component] {
+ let endpoint = ComponentsEndpoint(fileId: fileId)
+ return try client.request(endpoint)
+ }
+
+ private func calculatePrefixFrom(stateGroup: String) -> String? {
+ switch (stateGroup) {
+ case spacingsVerticalFrameName: return "v"
+ case spacingsHorizontalFrameName: return "h"
+ default: return nil
+ }
+ }
+
+ private func extractSizeFrom(componentName: String) -> Double? {
+ let regex = try? NSRegularExpression(pattern: #"[Ss]ize=(?\d+)"#)
+ let matches = regex?.matches(
+ in: componentName,
+ options: [],
+ range: NSRange(componentName.startIndex.. XcodeSpacingsOutput {
+ return XcodeSpacingsOutput(
+ spacingsUrl: iosParams.spacings?.spacingsSwift,
+ addObjcAttribute: iosParams.addObjcAttribute,
+ templatesPath: iosParams.templatesPath
+ )
+ }
+
+ private func exportXcodeSpacings(spacings: [Spacing], iosParams: Params.iOS) throws {
+ let output = createXcodeOutput(from: iosParams)
+ let exporter = XcodeSpacingsExporter(output: output)
+ let files = try exporter.export(spacings: spacings)
+
+ try fileWriter.write(files: files)
+
+ guard iosParams.xcassetsInSwiftPackage == false else { return }
+
+ do {
+ let xcodeProject = try XcodeProjectWriter(xcodeProjPath: iosParams.xcodeprojPath, target: iosParams.target)
+ try files.forEach { file in
+ if file.destination.file.pathExtension == "swift" {
+ try xcodeProject.addFileReferenceToXcodeProj(file.destination.url)
+ }
+ }
+ try xcodeProject.save()
+ } catch {
+ logger.error("Unable to add some file references to Xcode project")
+ }
+ }
+
+ private func exportAndroidSpacings(spacings: [Spacing], androidParams: Params.Android) throws {
+ let output = AndroidOutput(
+ xmlOutputDirectory: androidParams.mainRes,
+ xmlResourcePackage: androidParams.resourcePackage,
+ srcDirectory: androidParams.mainSrc,
+ packageName: androidParams.typography?.composePackageName,
+ templatesPath: androidParams.templatesPath
+ )
+ let exporter = AndroidSpacingsExporter(output: output)
+ let files = try exporter.exportSpacings(spacings: spacings)
+
+ let fileURL = androidParams.mainRes.appendingPathComponent("values/dimens.xml")
+
+ try? FileManager.default.removeItem(atPath: fileURL.path)
+ try fileWriter.write(files: files)
+ }
+ }
+}
diff --git a/Sources/FigmaExport/main.swift b/Sources/FigmaExport/main.swift
index 793693fe..c6125624 100644
--- a/Sources/FigmaExport/main.swift
+++ b/Sources/FigmaExport/main.swift
@@ -48,6 +48,7 @@ struct FigmaExportCommand: ParsableCommand {
ExportIcons.self,
ExportImages.self,
ExportTypography.self,
+ ExportSpacings.self,
GenerateConfigFile.self
],
defaultSubcommand: ExportColors.self
diff --git a/Sources/FigmaExportCore/Processor/AssetsProcessor.swift b/Sources/FigmaExportCore/Processor/AssetsProcessor.swift
index cc11cd51..0f92cb87 100644
--- a/Sources/FigmaExportCore/Processor/AssetsProcessor.swift
+++ b/Sources/FigmaExportCore/Processor/AssetsProcessor.swift
@@ -72,6 +72,22 @@ public struct TypographyProcessor: AssetsProcessable {
}
}
+public struct SpacingsProcessor: AssetsProcessable {
+ public typealias AssetType = Spacing
+
+ public let platform: Platform
+ public let nameValidateRegexp: String?
+ public let nameReplaceRegexp: String?
+ public let nameStyle: NameStyle?
+
+ public init(platform: Platform, nameValidateRegexp: String?, nameReplaceRegexp: String?, nameStyle: NameStyle?) {
+ self.platform = platform
+ self.nameValidateRegexp = nameValidateRegexp
+ self.nameReplaceRegexp = nameReplaceRegexp
+ self.nameStyle = nameStyle
+ }
+}
+
public struct ImagesProcessor: AssetsProcessable {
public typealias AssetType = ImagePack
diff --git a/Sources/FigmaExportCore/Spacing.swift b/Sources/FigmaExportCore/Spacing.swift
new file mode 100644
index 00000000..e32b3d9a
--- /dev/null
+++ b/Sources/FigmaExportCore/Spacing.swift
@@ -0,0 +1,29 @@
+import Foundation
+
+public struct Spacing: Asset {
+
+ public var name: String
+ public var platform: Platform?
+ public let size: Double
+
+ public init(
+ name: String,
+ platform: Platform? = nil,
+ size: Double
+ ) {
+
+ self.name = name
+ self.platform = platform
+ self.size = size
+ }
+
+ // MARK: Hashable
+
+ public static func == (lhs: Spacing, rhs: Spacing) -> Bool {
+ return lhs.name == rhs.name
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(name)
+ }
+}
diff --git a/Sources/FigmaExportCore/TextStyle.swift b/Sources/FigmaExportCore/TextStyle.swift
index 9081a776..c75b30f0 100644
--- a/Sources/FigmaExportCore/TextStyle.swift
+++ b/Sources/FigmaExportCore/TextStyle.swift
@@ -64,9 +64,10 @@ public struct TextStyle: Asset {
fontStyle: DynamicTypeStyle?,
lineHeight: Double? = nil,
letterSpacing: Double,
- textCase: TextCase = .original) {
-
+ textCase: TextCase = .original
+ ) {
self.name = name
+ self.platform = platform
self.fontName = fontName
self.fontSize = fontSize
self.fontStyle = fontStyle
diff --git a/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift b/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift
new file mode 100644
index 00000000..0c4c3c89
--- /dev/null
+++ b/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+public struct XcodeSpacingsOutput {
+ let spacingsUrl: URL?
+ let addObjcAttribute: Bool
+ let templatesPath: URL?
+
+ public init(
+ spacingsUrl: URL?,
+ addObjcAttribute: Bool? = false,
+ templatesPath: URL? = nil
+ ) {
+ self.spacingsUrl = spacingsUrl
+ self.addObjcAttribute = addObjcAttribute ?? false
+ self.templatesPath = templatesPath
+ }
+}
diff --git a/Sources/XcodeExport/Resources/Spacings.swift.stencil b/Sources/XcodeExport/Resources/Spacings.swift.stencil
new file mode 100644
index 00000000..74def582
--- /dev/null
+++ b/Sources/XcodeExport/Resources/Spacings.swift.stencil
@@ -0,0 +1,6 @@
+{% include "header.stencil" %}
+import Foundation
+
+public struct Spacings {{ "{" }}{% for spacing in spacings %}
+ {% if addObjcPrefix %}@objc {% endif %}static let {{ spacing.name }}: Double = {{ spacing.size }}{% endfor %}
+}
diff --git a/Sources/XcodeExport/XcodeSpacingsExporter.swift b/Sources/XcodeExport/XcodeSpacingsExporter.swift
new file mode 100644
index 00000000..5b0310c0
--- /dev/null
+++ b/Sources/XcodeExport/XcodeSpacingsExporter.swift
@@ -0,0 +1,25 @@
+import Foundation
+import FigmaExportCore
+import Stencil
+
+final public class XcodeSpacingsExporter: XcodeExporterBase {
+ private let output: XcodeSpacingsOutput
+
+ public init(output: XcodeSpacingsOutput) {
+ self.output = output
+ }
+
+ public func export(spacings: [Spacing]) throws -> [FileContents] {
+ guard let spacingsUrl = output.spacingsUrl else { return [] }
+ return [try makeSpacingsStruct(spacings: spacings, spacingsUrl: spacingsUrl)]
+ }
+
+ private func makeSpacingsStruct(spacings: [Spacing], spacingsUrl: URL) throws -> FileContents {
+ let env = makeEnvironment(templatesPath: output.templatesPath)
+ let contents = try env.renderTemplate(name: "Spacings.swift.stencil", context: [
+ "spacings": spacings.map { spacing in [ "name": spacing.name.lowerCamelCased(), "size": spacing.size ] },
+ "addObjcPrefix": output.addObjcAttribute
+ ])
+ return try makeFileContents(for: contents, url: spacingsUrl)
+ }
+}