From ca6907908d40f18f8856a5ceb5842e529e7c0dcb Mon Sep 17 00:00:00 2001 From: Ruben Sissing Date: Fri, 17 Nov 2023 13:28:34 +0100 Subject: [PATCH 1/3] feat(design-system/DT-6): remove generated Label.swift --- .../XcodeExport/Resources/Label.swift.stencil | 79 ------------------- .../XcodeExport/XcodeTypographyExporter.swift | 32 +------- .../XcodeTypographyExporterTests.swift | 27 +------ 3 files changed, 4 insertions(+), 134 deletions(-) delete mode 100644 Sources/XcodeExport/Resources/Label.swift.stencil diff --git a/Sources/XcodeExport/Resources/Label.swift.stencil b/Sources/XcodeExport/Resources/Label.swift.stencil deleted file mode 100644 index 704beddb..00000000 --- a/Sources/XcodeExport/Resources/Label.swift.stencil +++ /dev/null @@ -1,79 +0,0 @@ -{% include "header.stencil" %} -import UIKit - -public class Label: UILabel { - - var style: LabelStyle? { nil } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - updateText() - } - } - - public convenience init(text: String?, textColor: UIColor) { - self.init() - self.text = text - self.textColor = textColor - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - updateText() - } - - private func commonInit() { - font = style?.font - adjustsFontForContentSizeCategory = true - } - - private func updateText() { - text = super.text - } - - public override var text: String? { - get { - guard style?.attributes != nil else { - return super.text - } - - return attributedText?.string - } - set { - guard let style = style else { - super.text = newValue - return - } - - guard let newText = newValue else { - attributedText = nil - super.text = nil - return - } - - attributedText = style.attributedString(from: newText, alignment: textAlignment, lineBreakMode: lineBreakMode) - } - } -} -{% for style in styles %} -public final class {{ style.className }}Label: Label { - - override var style: LabelStyle? { - {% if separateStyles %}.{{ style.varName }}(){% else %}LabelStyle( - font: UIFont.{{ style.varName }}(){% if style.supportsDynamicType %}, - fontMetrics: UIFontMetrics(forTextStyle: .{{ style.type }}){% endif %}{% if style.lineHeight != 0 %}, - lineHeight: {{ style.lineHeight }}{% endif %}{% if style.tracking != 0 %}, - tracking: {{ style.tracking }}{% endif %}{% if style.textCase != "original" %}, - textCase: .{{ style.textCase }}{% endif %} - ){% endif %} - } -} -{% endfor %} \ No newline at end of file diff --git a/Sources/XcodeExport/XcodeTypographyExporter.swift b/Sources/XcodeExport/XcodeTypographyExporter.swift index 834ff69d..13c59c30 100644 --- a/Sources/XcodeExport/XcodeTypographyExporter.swift +++ b/Sources/XcodeExport/XcodeTypographyExporter.swift @@ -24,13 +24,6 @@ final public class XcodeTypographyExporter: XcodeExporterBase { // UIKit Labels if output.generateLabels, let labelsDirectory = output.urls.labels.labelsDirectory { - // Label.swift - files.append(try makeLabel( - textStyles: textStyles, - labelsDirectory: labelsDirectory, - separateStyles: output.urls.labels.labelStyleExtensionsURL != nil - )) - // LabelStyle.swift files.append(try makeLabelStyle(labelsDirectory: labelsDirectory)) @@ -80,7 +73,7 @@ final public class XcodeTypographyExporter: XcodeExporterBase { ]) return try makeFileContents(for: contents, url: swiftUIFontExtensionURL) } - + private func makeLabelStyleExtensionFileContents(textStyles: [TextStyle], labelStyleExtensionURL: URL) throws -> FileContents { let dict = textStyles.map { style -> [String: Any] in let type: String = style.fontStyle?.textStyleName ?? "" @@ -100,28 +93,7 @@ final public class XcodeTypographyExporter: XcodeExporterBase { let labelStylesSwiftExtension = try makeFileContents(for: contents, url: labelStyleExtensionURL) return labelStylesSwiftExtension } - - private func makeLabel(textStyles: [TextStyle], labelsDirectory: URL, separateStyles: Bool) throws -> FileContents { - let dict = textStyles.map { style -> [String: Any] in - let type: String = style.fontStyle?.textStyleName ?? "" - return [ - "className": style.name.first!.uppercased() + style.name.dropFirst(), - "varName": style.name, - "size": style.fontSize, - "supportsDynamicType": style.fontStyle != nil, - "type": type, - "tracking": style.letterSpacing.floatingPointFixed, - "lineHeight": style.lineHeight ?? 0, - "textCase": style.textCase.rawValue - ]} - let env = makeEnvironment(templatesPath: output.templatesPath) - let contents = try env.renderTemplate(name: "Label.swift.stencil", context: [ - "styles": dict, - "separateStyles": separateStyles - ]) - return try makeFileContents(for: contents, directory: labelsDirectory, file: URL(string: "Label.swift")!) - } - + private func makeLabelStyle(labelsDirectory: URL) throws -> FileContents { let env = makeEnvironment(templatesPath: output.templatesPath) let labelStyleSwiftContents = try env.renderTemplate(name: "LabelStyle.swift.stencil") diff --git a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift index 9fb0977e..a6c63f40 100644 --- a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift +++ b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift @@ -521,19 +521,7 @@ final class XcodeTypographyExporterTests: XCTestCase { """ XCTAssertEqual(files.count, 3, "Must be generated 3 files but generated \(files.count)") - - // Label.swift - XCTAssertNoDifference( - files[0], - FileContents( - destination: Destination( - directory: URL(string: "~/")!, - file: URL(string: "Label.swift")! - ), - data: contentsLabel.data(using: .utf8)! - ) - ) - + // LabelStyle.swift XCTAssertNoDifference( files[1], @@ -782,18 +770,7 @@ final class XcodeTypographyExporterTests: XCTestCase { """ XCTAssertEqual(files.count, 2, "Must be generated 2 files but generated \(files.count)") - - XCTAssertNoDifference( - files[0], - FileContents( - destination: Destination( - directory: URL(string: "~/")!, - file: URL(string: "Label.swift")! - ), - data: contentsLabel.data(using: .utf8)! - ) - ) - + XCTAssertNoDifference( files[1], FileContents( From 79f9edf5c73e75d04d98dd063fe348e2e92975c6 Mon Sep 17 00:00:00 2001 From: Ruben Sissing Date: Fri, 17 Nov 2023 15:18:40 +0100 Subject: [PATCH 2/3] feat(typography): Update iOS configuration to generate TextStyle extensions --- CONFIG.md | 12 ++-- .../FigmaExport/Release/figma-export.yaml | 8 +-- Examples/Example/figma-export.yaml | 12 ++-- .../FigmaExport/Release/figma-export.yaml | 8 +-- Examples/ExampleSwiftUI/figma-export.yaml | 4 +- README.md | 21 +++--- Sources/FigmaExport/Input/Params.swift | 6 +- Sources/FigmaExport/Resources/iOSConfig.swift | 12 ++-- .../Subcommands/ExportTypography.swift | 10 +-- .../Model/XcodeTypographyOutput.swift | 30 ++++---- .../Resources/Font+extension.swift.stencil | 15 +--- .../LabelStyle+extension.swift.stencil | 16 ----- .../Resources/LabelStyle.swift.stencil | 72 ------------------- .../TextStyle+extension.swift.stencil | 21 ++++++ .../Resources/TextStyle.swift.stencil | 32 +++++++++ .../Resources/UIFont+extension.swift.stencil | 26 +++---- .../XcodeExport/XcodeTypographyExporter.swift | 49 ++++++++----- .../XcodeTypographyExporterTests.swift | 30 ++++---- 18 files changed, 176 insertions(+), 208 deletions(-) delete mode 100644 Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil delete mode 100644 Sources/XcodeExport/Resources/LabelStyle.swift.stencil create mode 100644 Sources/XcodeExport/Resources/TextStyle+extension.swift.stencil create mode 100644 Sources/XcodeExport/Resources/TextStyle.swift.stencil diff --git a/CONFIG.md b/CONFIG.md index 3cb035fa..e05645cb 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -149,14 +149,14 @@ ios: typography: # [optional] Absolute or relative path to swift file where to export UIKit fonts (UIFont extension). fontSwift: "./Source/UIComponents/UIFont+extension.swift" - # [optional] Absolute or relative path to swift file where to generate LabelStyle extensions for each style (LabelStyle extension). - labelStyleSwift: "./Source/UIComponents/LabelStyle+extension.swift" + # [optional] Absolute or relative path to swift file where to generate TextStyle extensions for each style (TextStyle extension). + textStyleSwift: "./Source/UIComponents/TextStyle+extension.swift" # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./Source/View/Common/Font+extension.swift" - # Should FigmaExport generate UILabel for each text style (font)? E.g. HeaderLabel, BodyLabel, CaptionLabel - generateLabels: true - # Relative or absolute path to directory where to place UILabel for each text style (font) (Requred if generateLabels = true) - labelsDirectory: "./Source/UIComponents/" + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true + # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) + textStylesDirectory: "./Source/UIComponents/" # Typography name style: camelCase or snake_case nameStyle: camelCase diff --git a/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml b/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml index 64c6ddad..570a7f8c 100644 --- a/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml +++ b/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml @@ -80,10 +80,10 @@ ios: fontSwift: "./Source/UIComponents/UIFont+extension.swift" # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./Source/View/Common/Font+extension.swift" - # Should FigmaExport generate UILabel for each text style (font)? E.g. HeaderLabel, BodyLabel, CaptionLabel - generateLabels: true - # Relative or absolute path to directory where to place UILabel for each text style (font) (Requred if generateLabels = true) - labelsDirectory: "./Source/UIComponents/" + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true + # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) + textStylesDirectory: "./Source/UIComponents/" # [optional] Android export parameters android: diff --git a/Examples/Example/figma-export.yaml b/Examples/Example/figma-export.yaml index 8882475e..e9fb13c7 100644 --- a/Examples/Example/figma-export.yaml +++ b/Examples/Example/figma-export.yaml @@ -63,11 +63,11 @@ ios: typography: # Path to directory where to place UIFont+extension.swift file fontSwift: "./UIComponents/Source/Typography/UIFont+extension.swift" - # [optional] Absolute or relative path to swift file where to generate LabelStyle extensions for each style (LabelStyle extension). - labelStyleSwift: "./UIComponents/Source/Typography/LabelStyle+extension.swift" - # Will FigmaExport generate UILabel for each text style (font) e.g. HeaderLabel, BodyLabel, CaptionLabel. - generateLabels: true - # Path to directory where to place UILabel for each text style (font) (Required if generateLabels = true) - labelsDirectory: "./UIComponents/Source/Typography" + # [optional] Absolute or relative path to swift file where to generate TextStyle extensions for each style (TextStyle extension). + textStyleSwift: "./UIComponents/Source/Typography/TextStyle+extension.swift" + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true + # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) + textStylesDirectory: "./UIComponents/Source/Typography" # Typography name style: camelCase or snake_case nameStyle: camelCase diff --git a/Examples/ExampleSwiftUI/Pods/FigmaExport/Release/figma-export.yaml b/Examples/ExampleSwiftUI/Pods/FigmaExport/Release/figma-export.yaml index 64c6ddad..570a7f8c 100644 --- a/Examples/ExampleSwiftUI/Pods/FigmaExport/Release/figma-export.yaml +++ b/Examples/ExampleSwiftUI/Pods/FigmaExport/Release/figma-export.yaml @@ -80,10 +80,10 @@ ios: fontSwift: "./Source/UIComponents/UIFont+extension.swift" # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./Source/View/Common/Font+extension.swift" - # Should FigmaExport generate UILabel for each text style (font)? E.g. HeaderLabel, BodyLabel, CaptionLabel - generateLabels: true - # Relative or absolute path to directory where to place UILabel for each text style (font) (Requred if generateLabels = true) - labelsDirectory: "./Source/UIComponents/" + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true + # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) + textStylesDirectory: "./Source/UIComponents/" # [optional] Android export parameters android: diff --git a/Examples/ExampleSwiftUI/figma-export.yaml b/Examples/ExampleSwiftUI/figma-export.yaml index f82394b5..ac9899f0 100644 --- a/Examples/ExampleSwiftUI/figma-export.yaml +++ b/Examples/ExampleSwiftUI/figma-export.yaml @@ -65,7 +65,7 @@ ios: fontSwift: "./ExampleSwiftUI/View/Common/UIFont+extension.swift" # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./ExampleSwiftUI/View/Common/Font+extension.swift" - # Will FigmaExport generate UILabel for each text style (font) e.g. HeaderLabel, BodyLabel, CaptionLabel. - generateLabels: false + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true # Typography name style: camelCase or snake_case nameStyle: camelCase diff --git a/README.md b/README.md index 43150b20..af3c332a 100644 --- a/README.md +++ b/README.md @@ -204,13 +204,15 @@ If name of an image contains idiom at the end (e.g. ~ipad), it will be exported #### Typography When your execute `figma-export typography` command `figma-export` generates 3 files: -1. `UIFont+extension.swift` extension for UIFont that declares your custom fonts. Use these fonts like this `UIFont.header()`, `UIFont.caption1()`. -2. `LabelStyle.swift` struct for generating attributes for NSAttributesString with custom lineHeight and tracking (letter spacing). -3. `Label.swift` file that contains base Label class and class for each text style. E.g. HeaderLabel, BodyLabel, Caption1Label. Specify these classes in xib files on in code. +1. `TextStyle.swift` struct for generating TextStyles for SwiftUI with custom line spacing, kerning (letter spacing) and text case. +2. `TextStyle+extension.swift` extension including all the custom TextStyles from the Figma file. +3. `Font+extension` extension for Font that declares your custom fonts. +4. `UIFont+extension.swift` extension for UIFont that declares your custom fonts. Mainly used to get the default font lineHeight. Example of these files: -- [./Examples/Example/UIComponents/Source/Typography/Label.swift](./Examples/Example/UIComponents/Source/Typography/Label.swift) -- [./Examples/Example/UIComponents/Source/Typography/LabelStyle.swift](./Examples/Example/UIComponents/Source/Typography/LabelStyle.swift) +- [./Examples/Example/UIComponents/Source/Typography/TextStyle.swift](./Examples/Example/UIComponents/Source/Typography/TextStyle.swift) +- [./Examples/Example/UIComponents/Source/Typography/TextStyle+extension.swift](./Examples/Example/UIComponents/Source/Typography/TextStyle+extension.swift) +- [./Examples/Example/UIComponents/Source/Typography/Font+extension.swift](./Examples/Example/UIComponents/Source/Typography/Font+extension.swift) - [./Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift](./Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift) ### Android @@ -430,11 +432,10 @@ Custom Stencil templates for colors and images must have the following names: - Image+extension.swift.stencil for SwiftUI images Custom Stencil templates for typography must have the following names: -- Label.swift.stencil, -- LabelStyle.swift.stencil, -- LabelStyle+extension.swift.stencil, +- TextStyle.swift.stencil, +- TextStyle+extension.swift.stencil, +- Font+extension.swift.stencil - UIFont+extension.swift.stencil -- Font+extension.swift.stencil.stencil ##### Android @@ -453,7 +454,7 @@ Custom Stencil templates must have the following names: #### iOS 1. Add a custom font to the Xcode project. Drag & drop font file to the Xcode project, set target membership, and add font file name in the Info.plist file. [See developer documentation for more info.](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app)
2. Run `figma-export typography` to export text styles -3. Use generated fonts and labels in your code. E.g. `button.titleLabel?.font = UIFont.body()`, `let label = HeaderLabel()`. +3. Use generated fonts and text styles in your code. E.g. `Text("Header").textStyle(.header)`. #### Android 1. Place font file under the `res` directory of your module diff --git a/Sources/FigmaExport/Input/Params.swift b/Sources/FigmaExport/Input/Params.swift index 9865e0b4..ed813db1 100644 --- a/Sources/FigmaExport/Input/Params.swift +++ b/Sources/FigmaExport/Input/Params.swift @@ -93,10 +93,10 @@ struct Params: Decodable { struct Typography: Decodable { let fontSwift: URL? - let labelStyleSwift: URL? + let textStyleSwift: URL? let swiftUIFontSwift: URL? - let generateLabels: Bool - let labelsDirectory: URL? + let generateTextStyles: Bool + let textStylesDirectory: URL? let nameStyle: NameStyle } diff --git a/Sources/FigmaExport/Resources/iOSConfig.swift b/Sources/FigmaExport/Resources/iOSConfig.swift index 9be52333..4045245f 100644 --- a/Sources/FigmaExport/Resources/iOSConfig.swift +++ b/Sources/FigmaExport/Resources/iOSConfig.swift @@ -118,14 +118,14 @@ ios: typography: # [optional] Absolute or relative path to swift file where to export UIKit fonts (UIFont extension). fontSwift: "./Source/UIComponents/UIFont+extension.swift" - # [optional] Absolute or relative path to swift file where to generate LabelStyle extensions for each style (LabelStyle extension). - labelStyleSwift: "./Source/UIComponents/LabelStyle+extension.swift" + # [optional] Absolute or relative path to swift file where to generate TextStyle extensions for each style (TextStyle extension). + textStyleSwift: "./Source/UIComponents/TextStyle+extension.swift" # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./Source/View/Common/Font+extension.swift" - # Should FigmaExport generate UILabel for each text style (font)? E.g. HeaderLabel, BodyLabel, CaptionLabel - generateLabels: true - # Relative or absolute path to directory where to place UILabel for each text style (font) (Requred if generateLabels = true) - labelsDirectory: "./Source/UIComponents/" + # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption + generateTextStyles: true + # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) + textStylesDirectory: "./Source/UIComponents/" # Typography name style: camelCase or snake_case nameStyle: camelCase """# diff --git a/Sources/FigmaExport/Subcommands/ExportTypography.swift b/Sources/FigmaExport/Subcommands/ExportTypography.swift index 3a357135..54a50b6b 100644 --- a/Sources/FigmaExport/Subcommands/ExportTypography.swift +++ b/Sources/FigmaExport/Subcommands/ExportTypography.swift @@ -62,17 +62,17 @@ extension FigmaExportCommand { fontExtensionURL: iosParams.typography?.fontSwift, swiftUIFontExtensionURL: iosParams.typography?.swiftUIFontSwift ) - let labelUrls = XcodeTypographyOutput.LabelURLs( - labelsDirectory: iosParams.typography?.labelsDirectory, - labelStyleExtensionsURL: iosParams.typography?.labelStyleSwift + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( + textStylesDirectory: iosParams.typography?.textStylesDirectory, + textStyleExtensionsURL: iosParams.typography?.textStyleSwift ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) return XcodeTypographyOutput( urls: urls, - generateLabels: iosParams.typography?.generateLabels, + generateTextStyles: iosParams.typography?.generateTextStyles, addObjcAttribute: iosParams.addObjcAttribute, templatesPath: iosParams.templatesPath ) diff --git a/Sources/XcodeExport/Model/XcodeTypographyOutput.swift b/Sources/XcodeExport/Model/XcodeTypographyOutput.swift index c5a65459..4defbd20 100644 --- a/Sources/XcodeExport/Model/XcodeTypographyOutput.swift +++ b/Sources/XcodeExport/Model/XcodeTypographyOutput.swift @@ -2,7 +2,7 @@ import Foundation public struct XcodeTypographyOutput { let urls: URLs - let generateLabels: Bool + let generateTextStyles: Bool let addObjcAttribute: Bool let templatesPath: URL? @@ -18,40 +18,40 @@ public struct XcodeTypographyOutput { } } - public struct LabelURLs { - let labelsDirectory: URL? - let labelStyleExtensionsURL: URL? - + public struct TextStyleURLs { + let textStylesDirectory: URL? + let textStyleExtensionsURL: URL? + public init( - labelsDirectory: URL? = nil, - labelStyleExtensionsURL: URL? = nil + textStylesDirectory: URL? = nil, + textStyleExtensionsURL: URL? = nil ) { - self.labelsDirectory = labelsDirectory - self.labelStyleExtensionsURL = labelStyleExtensionsURL + self.textStylesDirectory = textStylesDirectory + self.textStyleExtensionsURL = textStyleExtensionsURL } } public struct URLs { public let fonts: FontURLs - public let labels: LabelURLs - + public let textStyles: TextStyleURLs + public init( fonts: FontURLs, - labels: LabelURLs + textStyles: TextStyleURLs ) { self.fonts = fonts - self.labels = labels + self.textStyles = textStyles } } public init( urls: URLs, - generateLabels: Bool? = false, + generateTextStyles: Bool? = false, addObjcAttribute: Bool? = false, templatesPath: URL? = nil ) { self.urls = urls - self.generateLabels = generateLabels ?? false + self.generateTextStyles = generateTextStyles ?? false self.addObjcAttribute = addObjcAttribute ?? false self.templatesPath = templatesPath } diff --git a/Sources/XcodeExport/Resources/Font+extension.swift.stencil b/Sources/XcodeExport/Resources/Font+extension.swift.stencil index b1c36c13..d1655ed8 100644 --- a/Sources/XcodeExport/Resources/Font+extension.swift.stencil +++ b/Sources/XcodeExport/Resources/Font+extension.swift.stencil @@ -1,16 +1,7 @@ {% include "header.stencil" %} import SwiftUI -public extension Font { -{% for textStyle in textStyles %}{% if textStyle.supportsDynamicType %} - static func {{ textStyle.name }}() -> Font { - if #available(iOS 14.0, *) { - return Font.custom("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}, relativeTo: .{{ textStyle.type }}) - } else { - return Font.custom("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}) - } - }{% else %} - static func {{ textStyle.name }}() -> Font { - Font.custom("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}) - }{% endif %}{% endfor %} +public extension Font {{ '{' }}{% for textStyle in textStyles %}{% if textStyle.supportsDynamicType %} + static let {{ textStyle.name }} = Font.custom("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}, relativeTo: .{{ textStyle.type }}){% else %} + static let {{ textStyle.name }} = Font.custom("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}){% endif %}{% endfor %} } diff --git a/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil b/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil deleted file mode 100644 index c7ded591..00000000 --- a/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil +++ /dev/null @@ -1,16 +0,0 @@ -{% include "header.stencil" %} -import UIKit - -public extension LabelStyle { - {% for style in styles %} - static func {{ style.varName }}() -> LabelStyle { - LabelStyle( - font: UIFont.{{ style.varName }}(){% if style.supportsDynamicType %}, - fontMetrics: UIFontMetrics(forTextStyle: .{{ style.type }}){% endif %}{% if style.lineHeight != 0 %}, - lineHeight: {{ style.lineHeight }}{% endif %}{% if style.tracking != 0 %}, - tracking: {{ style.tracking }}{% endif %}{% if style.textCase != "original" %}, - textCase: .{{ style.textCase }}{% endif %} - ) - } - {% endfor %} -} \ No newline at end of file diff --git a/Sources/XcodeExport/Resources/LabelStyle.swift.stencil b/Sources/XcodeExport/Resources/LabelStyle.swift.stencil deleted file mode 100644 index 0e25096e..00000000 --- a/Sources/XcodeExport/Resources/LabelStyle.swift.stencil +++ /dev/null @@ -1,72 +0,0 @@ -{% include "header.stencil" %} -import UIKit - -public struct LabelStyle { - - enum TextCase { - case uppercased - case lowercased - case original - } - - let font: UIFont - let fontMetrics: UIFontMetrics? - let lineHeight: CGFloat? - let tracking: CGFloat - let textCase: TextCase - - init(font: UIFont, fontMetrics: UIFontMetrics? = nil, lineHeight: CGFloat? = nil, tracking: CGFloat = 0, textCase: TextCase = .original) { - self.font = font - self.fontMetrics = fontMetrics - self.lineHeight = lineHeight - self.tracking = tracking - self.textCase = textCase - } - - public func attributes( - for alignment: NSTextAlignment = .left, - lineBreakMode: NSLineBreakMode = .byTruncatingTail - ) -> [NSAttributedString.Key: Any] { - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = alignment - paragraphStyle.lineBreakMode = lineBreakMode - - var baselineOffset: CGFloat = .zero - - if let lineHeight { - let scaledLineHeight: CGFloat = fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight - paragraphStyle.minimumLineHeight = scaledLineHeight - paragraphStyle.maximumLineHeight = scaledLineHeight - - baselineOffset = (scaledLineHeight - font.lineHeight) / 4.0 - } - - return [ - NSAttributedString.Key.paragraphStyle: paragraphStyle, - NSAttributedString.Key.kern: tracking, - NSAttributedString.Key.baselineOffset: baselineOffset, - NSAttributedString.Key.font: font - ] - } - - public func attributedString( - from string: String, - alignment: NSTextAlignment = .left, - lineBreakMode: NSLineBreakMode = .byTruncatingTail - ) -> NSAttributedString { - let attributes = attributes(for: alignment, lineBreakMode: lineBreakMode) - return NSAttributedString(string: convertText(string), attributes: attributes) - } - - private func convertText(_ text: String) -> String { - switch textCase { - case .uppercased: - return text.uppercased() - case .lowercased: - return text.lowercased() - default: - return text - } - } -} diff --git a/Sources/XcodeExport/Resources/TextStyle+extension.swift.stencil b/Sources/XcodeExport/Resources/TextStyle+extension.swift.stencil new file mode 100644 index 00000000..3456255a --- /dev/null +++ b/Sources/XcodeExport/Resources/TextStyle+extension.swift.stencil @@ -0,0 +1,21 @@ +{% include "header.stencil" %} +import SwiftUI + +extension TextStyle {{ '{' }}{% for style in styles %} + public static let {{ style.varName }} = TextStyle( + font: .{{ style.varName }}{% if style.lineHeight != 0 %}, + lineSpacing: UIFont.{{ style.varName }}.lineSpacing(lineHeight: {{ style.lineHeight }}){% endif %}{% if style.tracking != 0 %}, + kerning: {{ style.tracking }}{% endif %}{% if style.textCase != "original" %}, + textCase: .{{ style.textCase }}{% endif %} + ){% endfor %} +} + +#Preview { + DesignSystem.initialize() + return ScrollView { + VStack {{ '{' }}{% for style in styles %} + Text("{{ style.className }}") + .textStyle(.{{ style.varName }}){% endfor %} + } + } +} diff --git a/Sources/XcodeExport/Resources/TextStyle.swift.stencil b/Sources/XcodeExport/Resources/TextStyle.swift.stencil new file mode 100644 index 00000000..bf3ccce7 --- /dev/null +++ b/Sources/XcodeExport/Resources/TextStyle.swift.stencil @@ -0,0 +1,32 @@ +{% include "header.stencil" %} +import SwiftUI + +public struct TextStyle { + fileprivate let font: Font + fileprivate let lineSpacing: CGFloat + fileprivate let kerning: CGFloat + fileprivate let textCase: Text.Case? + + internal init( + font: Font, + lineSpacing: CGFloat = .zero, + kerning: CGFloat = .zero, + textCase: Text.Case? = nil + ) { + self.font = font + self.lineSpacing = lineSpacing + self.kerning = kerning + self.textCase = textCase + } +} + +extension View { + public func textStyle(_ textStyle: TextStyle) -> some View { + self + .font(textStyle.font) + .kerning(textStyle.kerning) + .lineSpacing(textStyle.lineSpacing) + .textCase(textStyle.textCase) + .padding(.vertical, textStyle.lineSpacing / 2) + } +} diff --git a/Sources/XcodeExport/Resources/UIFont+extension.swift.stencil b/Sources/XcodeExport/Resources/UIFont+extension.swift.stencil index 94caf9cd..f47c7d88 100644 --- a/Sources/XcodeExport/Resources/UIFont+extension.swift.stencil +++ b/Sources/XcodeExport/Resources/UIFont+extension.swift.stencil @@ -1,28 +1,28 @@ {% include "header.stencil" %} import UIKit -public extension UIFont { -{% for textStyle in textStyles %} - {% if addObjcPrefix %}@objc {% endif %}static func {{ textStyle.name }}() -> UIFont { - customFont("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}{% if textStyle.supportsDynamicType %}, textStyle: .{{ textStyle.type }}, scaled: true{% endif %}) - } -{% endfor %} +public extension UIFont {{ '{' }}{% for textStyle in textStyles %} + static let {{ textStyle.name }}: UIFont = customFont("{{ textStyle.fontName }}", size: {{ textStyle.fontSize }}{% if textStyle.supportsDynamicType %}, textStyle: .{{ textStyle.type }}{% endif %}){% endfor %} + private static func customFont( _ name: String, size: CGFloat, - textStyle: UIFont.TextStyle? = nil, - scaled: Bool = false) -> UIFont { - + textStyle: UIFont.TextStyle? = nil + ) -> UIFont { guard let font = UIFont(name: name, size: size) else { print("Warning: Font \(name) not found.") return UIFont.systemFont(ofSize: size, weight: .regular) } - - if scaled, let textStyle = textStyle { + + if let textStyle { let metrics = UIFontMetrics(forTextStyle: textStyle) return metrics.scaledFont(for: font) - } else { - return font } + + return font + } + + internal func lineSpacing(lineHeight customLineHeight: CGFloat) -> CGFloat { + max(0, customLineHeight - self.lineHeight) } } diff --git a/Sources/XcodeExport/XcodeTypographyExporter.swift b/Sources/XcodeExport/XcodeTypographyExporter.swift index 13c59c30..cbe608d7 100644 --- a/Sources/XcodeExport/XcodeTypographyExporter.swift +++ b/Sources/XcodeExport/XcodeTypographyExporter.swift @@ -22,16 +22,16 @@ final public class XcodeTypographyExporter: XcodeExporterBase { files.append(try makeFontExtension(textStyles: textStyles, swiftUIFontExtensionURL: url)) } - // UIKit Labels - if output.generateLabels, let labelsDirectory = output.urls.labels.labelsDirectory { - // LabelStyle.swift - files.append(try makeLabelStyle(labelsDirectory: labelsDirectory)) - - // LabelStyle extensions - if let url = output.urls.labels.labelStyleExtensionsURL { - files.append(try makeLabelStyleExtensionFileContents( + // SwiftUI TextStyles + if output.generateTextStyles, let textStylesDirectory = output.urls.textStyles.textStylesDirectory { + // TextStyle.swift + files.append(try makeTextStyle(textStylesDirectory: textStylesDirectory)) + + // TextStyle extensions + if let url = output.urls.textStyles.textStyleExtensionsURL { + files.append(try makeTextStyleExtensionFileContents( textStyles: textStyles, - labelStyleExtensionURL: url + textStyleExtensionURL: url )) } } @@ -74,9 +74,19 @@ final public class XcodeTypographyExporter: XcodeExporterBase { return try makeFileContents(for: contents, url: swiftUIFontExtensionURL) } - private func makeLabelStyleExtensionFileContents(textStyles: [TextStyle], labelStyleExtensionURL: URL) throws -> FileContents { + private func makeTextStyleExtensionFileContents(textStyles: [TextStyle], textStyleExtensionURL: URL) throws -> FileContents { let dict = textStyles.map { style -> [String: Any] in let type: String = style.fontStyle?.textStyleName ?? "" + let textCase: String = { + switch style.textCase { + case .lowercased: + return "lowercase" + case .uppercased: + return "uppercase" + case .original: + return "original" + } + }() return [ "className": style.name.first!.uppercased() + style.name.dropFirst(), "varName": style.name, @@ -85,18 +95,19 @@ final public class XcodeTypographyExporter: XcodeExporterBase { "type": type, "tracking": style.letterSpacing.floatingPointFixed, "lineHeight": style.lineHeight ?? 0, - "textCase": style.textCase.rawValue - ]} + "textCase": textCase + ] + } let env = makeEnvironment(templatesPath: output.templatesPath) - let contents = try env.renderTemplate(name: "LabelStyle+extension.swift.stencil", context: ["styles": dict]) - - let labelStylesSwiftExtension = try makeFileContents(for: contents, url: labelStyleExtensionURL) - return labelStylesSwiftExtension + let contents = try env.renderTemplate(name: "TextStyle+extension.swift.stencil", context: ["styles": dict]) + + let textStylesSwiftExtension = try makeFileContents(for: contents, url: textStyleExtensionURL) + return textStylesSwiftExtension } - private func makeLabelStyle(labelsDirectory: URL) throws -> FileContents { + private func makeTextStyle(textStylesDirectory: URL) throws -> FileContents { let env = makeEnvironment(templatesPath: output.templatesPath) - let labelStyleSwiftContents = try env.renderTemplate(name: "LabelStyle.swift.stencil") - return try makeFileContents(for: labelStyleSwiftContents, directory: labelsDirectory, file: URL(string: "LabelStyle.swift")!) + let textStyleSwiftContents = try env.renderTemplate(name: "TextStyle.swift.stencil") + return try makeFileContents(for: textStyleSwiftContents, directory: textStylesDirectory, file: URL(string: "TextStyle.swift")!) } } diff --git a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift index a6c63f40..314c27e9 100644 --- a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift +++ b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift @@ -26,10 +26,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( fontExtensionURL: URL(string: "~/UIFont+extension.swift")! ) - let labelUrls = XcodeTypographyOutput.LabelURLs() + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) let output = XcodeTypographyOutput(urls: urls) let exporter = XcodeTypographyExporter(output: output) @@ -110,10 +110,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( fontExtensionURL: URL(string: "~/UIFont+extension.swift")! ) - let labelUrls = XcodeTypographyOutput.LabelURLs() + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) let output = XcodeTypographyOutput( urls: urls, @@ -197,10 +197,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( swiftUIFontExtensionURL: URL(string: "~/Font+extension.swift")! ) - let labelUrls = XcodeTypographyOutput.LabelURLs() + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) let output = XcodeTypographyOutput(urls: urls) let exporter = XcodeTypographyExporter(output: output) @@ -269,17 +269,17 @@ final class XcodeTypographyExporterTests: XCTestCase { func testExportStyleExtensions() throws { let fontUrls = XcodeTypographyOutput.FontURLs() - let labelUrls = XcodeTypographyOutput.LabelURLs( - labelsDirectory: URL(string: "~/")!, - labelStyleExtensionsURL: URL(string: "~/LabelStyle+extension.swift")! + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( + textStylesDirectory: URL(string: "~/")!, + textStyleExtensionsURL: URL(string: "~/TextStyle+extension.swift")! ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) let output = XcodeTypographyOutput( urls: urls, - generateLabels: true + generateTextStyles: true ) let exporter = XcodeTypographyExporter(output: output) @@ -549,16 +549,16 @@ final class XcodeTypographyExporterTests: XCTestCase { func testExportLabel() throws { let fontUrls = XcodeTypographyOutput.FontURLs() - let labelUrls = XcodeTypographyOutput.LabelURLs( - labelsDirectory: URL(string: "~/")! + let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( + textStylesDirectory: URL(string: "~/")! ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - labels: labelUrls + textStyles: textStyleUrls ) let output = XcodeTypographyOutput( urls: urls, - generateLabels: true + generateTextStyles: true ) let exporter = XcodeTypographyExporter(output: output) From afd3efa79718a366d51cec89dafd1edf03306ba6 Mon Sep 17 00:00:00 2001 From: Ruben Sissing Date: Fri, 17 Nov 2023 17:55:13 +0100 Subject: [PATCH 3/3] feat(typography): Update font generation to support SwiftUI and UIKit --- .../FigmaExport/Release/figma-export.yaml | 2 +- .../Source/Typography/LabelStyle.swift | 1 + .../Source/Typography/UIFont+extension.swift | 43 ++++------ Examples/Example/figma-export.yaml | 16 ++-- Sources/FigmaExport/Input/Params.swift | 7 +- .../Subcommands/ExportTypography.swift | 14 ++-- .../Model/XcodeTypographyOutput.swift | 61 +++++++++++--- .../XcodeExport/Resources/Label.swift.stencil | 79 +++++++++++++++++++ .../LabelStyle+extension.swift.stencil | 16 ++++ .../Resources/LabelStyle.swift.stencil | 72 +++++++++++++++++ .../XcodeExport/XcodeTypographyExporter.swift | 68 ++++++++++------ .../XcodeTypographyExporterTests.swift | 30 +++---- 12 files changed, 315 insertions(+), 94 deletions(-) create mode 100644 Sources/XcodeExport/Resources/Label.swift.stencil create mode 100644 Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil create mode 100644 Sources/XcodeExport/Resources/LabelStyle.swift.stencil diff --git a/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml b/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml index 570a7f8c..b074af19 100644 --- a/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml +++ b/Examples/Example/Pods/FigmaExport/Release/figma-export.yaml @@ -81,7 +81,7 @@ ios: # [optional] Absolute or relative path to swift file where to export SwiftUI fonts (Font extension). swiftUIFontSwift: "./Source/View/Common/Font+extension.swift" # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption - generateTextStyles: true + generateStyles: true # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) textStylesDirectory: "./Source/UIComponents/" diff --git a/Examples/Example/UIComponents/Source/Typography/LabelStyle.swift b/Examples/Example/UIComponents/Source/Typography/LabelStyle.swift index d57c8746..4415773a 100644 --- a/Examples/Example/UIComponents/Source/Typography/LabelStyle.swift +++ b/Examples/Example/UIComponents/Source/Typography/LabelStyle.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // // The code generated using FigmaExport — Command line utility to export // colors, typography, icons and images from Figma to Xcode project. diff --git a/Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift b/Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift index 088b868d..90b2b5d8 100644 --- a/Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift +++ b/Examples/Example/UIComponents/Source/Typography/UIFont+extension.swift @@ -1,3 +1,4 @@ +// swiftlint:disable all // // The code generated using FigmaExport — Command line utility to export // colors, typography, icons and images from Figma to Xcode project. @@ -10,43 +11,31 @@ import UIKit public extension UIFont { - - static func body() -> UIFont { - customFont("PTSans-Regular", size: 16.0, textStyle: .body, scaled: true) - } - - static func caption() -> UIFont { - customFont("PTSans-Regular", size: 14.0, textStyle: .footnote, scaled: true) - } - - static func header() -> UIFont { - customFont("PTSans-Bold", size: 20.0) - } - - static func largeTitle() -> UIFont { - customFont("PTSans-Bold", size: 34.0, textStyle: .largeTitle, scaled: true) - } - - static func uppercased() -> UIFont { - customFont("PTSans-Regular", size: 14.0) - } + static let body: UIFont = customFont("PTSans-Regular", size: 16.0, textStyle: .body) + static let caption: UIFont = customFont("PTSans-Regular", size: 14.0, textStyle: .footnote) + static let header: UIFont = customFont("PTSans-Bold", size: 20.0) + static let largeTitle: UIFont = customFont("PTSans-Bold", size: 34.0, textStyle: .largeTitle) + static let uppercased: UIFont = customFont("PTSans-Regular", size: 14.0) private static func customFont( _ name: String, size: CGFloat, - textStyle: UIFont.TextStyle? = nil, - scaled: Bool = false) -> UIFont { - + textStyle: UIFont.TextStyle? = nil + ) -> UIFont { guard let font = UIFont(name: name, size: size) else { print("Warning: Font \(name) not found.") return UIFont.systemFont(ofSize: size, weight: .regular) } - - if scaled, let textStyle = textStyle { + + if let textStyle { let metrics = UIFontMetrics(forTextStyle: textStyle) return metrics.scaledFont(for: font) - } else { - return font } + + return font + } + + internal func lineSpacing(lineHeight customLineHeight: CGFloat) -> CGFloat { + max(0, customLineHeight - self.lineHeight) } } diff --git a/Examples/Example/figma-export.yaml b/Examples/Example/figma-export.yaml index e9fb13c7..e4215e5f 100644 --- a/Examples/Example/figma-export.yaml +++ b/Examples/Example/figma-export.yaml @@ -61,13 +61,15 @@ ios: # Parameters for exporting typography typography: - # Path to directory where to place UIFont+extension.swift file + # Choose what font system you want to use SwiftUI or UIKit + fontSystem: UIKit + # Path to directory where to place UIFont+extension.swift file required for both SwiftUI and UIKit fontSwift: "./UIComponents/Source/Typography/UIFont+extension.swift" - # [optional] Absolute or relative path to swift file where to generate TextStyle extensions for each style (TextStyle extension). - textStyleSwift: "./UIComponents/Source/Typography/TextStyle+extension.swift" - # Should FigmaExport generate TextStyles for each text style (font)? E.g. Header, Body, Caption - generateTextStyles: true - # Relative or absolute path to directory where to place TextStyles for each text style (font) (Required if generateTextStyles = true) - textStylesDirectory: "./UIComponents/Source/Typography" + # [optional] Absolute or relative path to swift file where to generate LabelStyle extensions for each style (LabelStyle extension). + labelStyleSwift: "./UIComponents/Source/Typography/LabelStyle+extension.swift" + # Should FigmaExport generate TextStyles or Labels (based on fontSystem) for each text style (font)? E.g. Header, Body, Caption + generateStyles: true + # Relative or absolute path to directory where to place TextStyles or Labels (based on fontSystem) for each text style (font) (Required if generateStyles = true) + stylesDirectory: "./UIComponents/Source/Typography" # Typography name style: camelCase or snake_case nameStyle: camelCase diff --git a/Sources/FigmaExport/Input/Params.swift b/Sources/FigmaExport/Input/Params.swift index ed813db1..770ea63e 100644 --- a/Sources/FigmaExport/Input/Params.swift +++ b/Sources/FigmaExport/Input/Params.swift @@ -1,5 +1,6 @@ import Foundation import FigmaExportCore +import XcodeExport extension NameStyle: Decodable {} @@ -92,11 +93,13 @@ struct Params: Decodable { } struct Typography: Decodable { + let fontSystem: FontSystem? let fontSwift: URL? let textStyleSwift: URL? + let labelStyleSwift: URL? let swiftUIFontSwift: URL? - let generateTextStyles: Bool - let textStylesDirectory: URL? + let generateStyles: Bool + let stylesDirectory: URL? let nameStyle: NameStyle } diff --git a/Sources/FigmaExport/Subcommands/ExportTypography.swift b/Sources/FigmaExport/Subcommands/ExportTypography.swift index 54a50b6b..c4e16134 100644 --- a/Sources/FigmaExport/Subcommands/ExportTypography.swift +++ b/Sources/FigmaExport/Subcommands/ExportTypography.swift @@ -37,7 +37,8 @@ extension FigmaExportCommand { nameStyle: typographyParams.nameStyle ) let processedTextStyles = try processor.process(assets: textStyles).get() - logger.info("Saving text styles...") + let fontSystemString = typographyParams.fontSystem.map { " for \($0.rawValue)"} ?? "" + logger.info("Saving text styles\(fontSystemString)...") try exportXcodeTextStyles(textStyles: processedTextStyles, iosParams: ios) logger.info("Done!") } @@ -62,17 +63,18 @@ extension FigmaExportCommand { fontExtensionURL: iosParams.typography?.fontSwift, swiftUIFontExtensionURL: iosParams.typography?.swiftUIFontSwift ) - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( - textStylesDirectory: iosParams.typography?.textStylesDirectory, - textStyleExtensionsURL: iosParams.typography?.textStyleSwift + let styleUrls = XcodeTypographyOutput.StyleURLs( + directory: iosParams.typography?.stylesDirectory, + extensionsURL: iosParams.typography?.textStyleSwift ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) return XcodeTypographyOutput( + fontSystem: iosParams.typography?.fontSystem, urls: urls, - generateTextStyles: iosParams.typography?.generateTextStyles, + generateStyles: iosParams.typography?.generateStyles, addObjcAttribute: iosParams.addObjcAttribute, templatesPath: iosParams.templatesPath ) diff --git a/Sources/XcodeExport/Model/XcodeTypographyOutput.swift b/Sources/XcodeExport/Model/XcodeTypographyOutput.swift index 4defbd20..e2c538ce 100644 --- a/Sources/XcodeExport/Model/XcodeTypographyOutput.swift +++ b/Sources/XcodeExport/Model/XcodeTypographyOutput.swift @@ -1,8 +1,41 @@ import Foundation +public enum FontSystem: String, Decodable { + case uiKit = "UIKit" + case swiftUI = "SwiftUI" + + var styleExtensionStencilFile: String { + switch self { + case .swiftUI: + return "TextStyle+extension.swift.stencil" + case .uiKit: + return "LabelStyle+extension.swift.stencil" + } + } + + var styleStencilFile: String { + switch self { + case .swiftUI: + return "TextStyle.swift.stencil" + case .uiKit: + return "LabelStyle.swift.stencil" + } + } + + var styleFile: String { + switch self { + case .swiftUI: + return "TextStyle.swift" + case .uiKit: + return "LabelStyle.swift" + } + } +} + public struct XcodeTypographyOutput { + let fontSystem: FontSystem let urls: URLs - let generateTextStyles: Bool + let generateStyles: Bool let addObjcAttribute: Bool let templatesPath: URL? @@ -18,40 +51,42 @@ public struct XcodeTypographyOutput { } } - public struct TextStyleURLs { - let textStylesDirectory: URL? - let textStyleExtensionsURL: URL? + public struct StyleURLs { + let directory: URL? + let extensionsURL: URL? public init( - textStylesDirectory: URL? = nil, - textStyleExtensionsURL: URL? = nil + directory: URL? = nil, + extensionsURL: URL? = nil ) { - self.textStylesDirectory = textStylesDirectory - self.textStyleExtensionsURL = textStyleExtensionsURL + self.directory = directory + self.extensionsURL = extensionsURL } } public struct URLs { public let fonts: FontURLs - public let textStyles: TextStyleURLs + public let styles: StyleURLs public init( fonts: FontURLs, - textStyles: TextStyleURLs + styles: StyleURLs ) { self.fonts = fonts - self.textStyles = textStyles + self.styles = styles } } public init( + fontSystem: FontSystem? = .swiftUI, urls: URLs, - generateTextStyles: Bool? = false, + generateStyles: Bool? = false, addObjcAttribute: Bool? = false, templatesPath: URL? = nil ) { + self.fontSystem = fontSystem ?? .swiftUI self.urls = urls - self.generateTextStyles = generateTextStyles ?? false + self.generateStyles = generateStyles ?? false self.addObjcAttribute = addObjcAttribute ?? false self.templatesPath = templatesPath } diff --git a/Sources/XcodeExport/Resources/Label.swift.stencil b/Sources/XcodeExport/Resources/Label.swift.stencil new file mode 100644 index 00000000..704beddb --- /dev/null +++ b/Sources/XcodeExport/Resources/Label.swift.stencil @@ -0,0 +1,79 @@ +{% include "header.stencil" %} +import UIKit + +public class Label: UILabel { + + var style: LabelStyle? { nil } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { + updateText() + } + } + + public convenience init(text: String?, textColor: UIColor) { + self.init() + self.text = text + self.textColor = textColor + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + updateText() + } + + private func commonInit() { + font = style?.font + adjustsFontForContentSizeCategory = true + } + + private func updateText() { + text = super.text + } + + public override var text: String? { + get { + guard style?.attributes != nil else { + return super.text + } + + return attributedText?.string + } + set { + guard let style = style else { + super.text = newValue + return + } + + guard let newText = newValue else { + attributedText = nil + super.text = nil + return + } + + attributedText = style.attributedString(from: newText, alignment: textAlignment, lineBreakMode: lineBreakMode) + } + } +} +{% for style in styles %} +public final class {{ style.className }}Label: Label { + + override var style: LabelStyle? { + {% if separateStyles %}.{{ style.varName }}(){% else %}LabelStyle( + font: UIFont.{{ style.varName }}(){% if style.supportsDynamicType %}, + fontMetrics: UIFontMetrics(forTextStyle: .{{ style.type }}){% endif %}{% if style.lineHeight != 0 %}, + lineHeight: {{ style.lineHeight }}{% endif %}{% if style.tracking != 0 %}, + tracking: {{ style.tracking }}{% endif %}{% if style.textCase != "original" %}, + textCase: .{{ style.textCase }}{% endif %} + ){% endif %} + } +} +{% endfor %} \ No newline at end of file diff --git a/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil b/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil new file mode 100644 index 00000000..c7ded591 --- /dev/null +++ b/Sources/XcodeExport/Resources/LabelStyle+extension.swift.stencil @@ -0,0 +1,16 @@ +{% include "header.stencil" %} +import UIKit + +public extension LabelStyle { + {% for style in styles %} + static func {{ style.varName }}() -> LabelStyle { + LabelStyle( + font: UIFont.{{ style.varName }}(){% if style.supportsDynamicType %}, + fontMetrics: UIFontMetrics(forTextStyle: .{{ style.type }}){% endif %}{% if style.lineHeight != 0 %}, + lineHeight: {{ style.lineHeight }}{% endif %}{% if style.tracking != 0 %}, + tracking: {{ style.tracking }}{% endif %}{% if style.textCase != "original" %}, + textCase: .{{ style.textCase }}{% endif %} + ) + } + {% endfor %} +} \ No newline at end of file diff --git a/Sources/XcodeExport/Resources/LabelStyle.swift.stencil b/Sources/XcodeExport/Resources/LabelStyle.swift.stencil new file mode 100644 index 00000000..0e25096e --- /dev/null +++ b/Sources/XcodeExport/Resources/LabelStyle.swift.stencil @@ -0,0 +1,72 @@ +{% include "header.stencil" %} +import UIKit + +public struct LabelStyle { + + enum TextCase { + case uppercased + case lowercased + case original + } + + let font: UIFont + let fontMetrics: UIFontMetrics? + let lineHeight: CGFloat? + let tracking: CGFloat + let textCase: TextCase + + init(font: UIFont, fontMetrics: UIFontMetrics? = nil, lineHeight: CGFloat? = nil, tracking: CGFloat = 0, textCase: TextCase = .original) { + self.font = font + self.fontMetrics = fontMetrics + self.lineHeight = lineHeight + self.tracking = tracking + self.textCase = textCase + } + + public func attributes( + for alignment: NSTextAlignment = .left, + lineBreakMode: NSLineBreakMode = .byTruncatingTail + ) -> [NSAttributedString.Key: Any] { + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = alignment + paragraphStyle.lineBreakMode = lineBreakMode + + var baselineOffset: CGFloat = .zero + + if let lineHeight { + let scaledLineHeight: CGFloat = fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight + paragraphStyle.minimumLineHeight = scaledLineHeight + paragraphStyle.maximumLineHeight = scaledLineHeight + + baselineOffset = (scaledLineHeight - font.lineHeight) / 4.0 + } + + return [ + NSAttributedString.Key.paragraphStyle: paragraphStyle, + NSAttributedString.Key.kern: tracking, + NSAttributedString.Key.baselineOffset: baselineOffset, + NSAttributedString.Key.font: font + ] + } + + public func attributedString( + from string: String, + alignment: NSTextAlignment = .left, + lineBreakMode: NSLineBreakMode = .byTruncatingTail + ) -> NSAttributedString { + let attributes = attributes(for: alignment, lineBreakMode: lineBreakMode) + return NSAttributedString(string: convertText(string), attributes: attributes) + } + + private func convertText(_ text: String) -> String { + switch textCase { + case .uppercased: + return text.uppercased() + case .lowercased: + return text.lowercased() + default: + return text + } + } +} diff --git a/Sources/XcodeExport/XcodeTypographyExporter.swift b/Sources/XcodeExport/XcodeTypographyExporter.swift index cbe608d7..48ea519c 100644 --- a/Sources/XcodeExport/XcodeTypographyExporter.swift +++ b/Sources/XcodeExport/XcodeTypographyExporter.swift @@ -3,6 +3,20 @@ import FigmaExportCore import Stencil final public class XcodeTypographyExporter: XcodeExporterBase { + private enum Error: LocalizedError { + case missingFont + case missingUIFont + + var errorDescription: String? { + switch self { + case .missingFont: + return "swiftUIFontExtensionURL required when using SwiftUI fontSystem" + case .missingUIFont: + return "fontExtensionURL required when using UIKit or SwiftUI fontSystem" + } + } + } + private let output: XcodeTypographyOutput public init(output: XcodeTypographyOutput) { @@ -12,24 +26,27 @@ final public class XcodeTypographyExporter: XcodeExporterBase { public func export(textStyles: [TextStyle]) throws -> [FileContents] { var files: [FileContents] = [] - // UIKit UIFont extension - if let url = output.urls.fonts.fontExtensionURL { - files.append(try makeUIFontExtension(textStyles: textStyles, fontExtensionURL: url)) + // UIKit & SwiftUI UIFont extension + guard let url = output.urls.fonts.fontExtensionURL else { + throw Error.missingUIFont } + files.append(try makeUIFontExtension(textStyles: textStyles, fontExtensionURL: url)) // SwiftUI Font extension - if let url = output.urls.fonts.swiftUIFontExtensionURL { + if output.fontSystem == .swiftUI { + guard let url = output.urls.fonts.swiftUIFontExtensionURL else { + throw Error.missingFont + } files.append(try makeFontExtension(textStyles: textStyles, swiftUIFontExtensionURL: url)) } - // SwiftUI TextStyles - if output.generateTextStyles, let textStylesDirectory = output.urls.textStyles.textStylesDirectory { - // TextStyle.swift - files.append(try makeTextStyle(textStylesDirectory: textStylesDirectory)) + // Styles + if output.generateStyles, let stylesDirectory = output.urls.styles.directory { + files.append(try makeStyle(fontSystem: output.fontSystem, directory: stylesDirectory)) - // TextStyle extensions - if let url = output.urls.textStyles.textStyleExtensionsURL { - files.append(try makeTextStyleExtensionFileContents( + if let url = output.urls.styles.extensionsURL { + files.append(try makeStyleExtensionFileContents( + fontSystem: output.fontSystem, textStyles: textStyles, textStyleExtensionURL: url )) @@ -74,17 +91,22 @@ final public class XcodeTypographyExporter: XcodeExporterBase { return try makeFileContents(for: contents, url: swiftUIFontExtensionURL) } - private func makeTextStyleExtensionFileContents(textStyles: [TextStyle], textStyleExtensionURL: URL) throws -> FileContents { + private func makeStyleExtensionFileContents(fontSystem: FontSystem, textStyles: [TextStyle], textStyleExtensionURL: URL) throws -> FileContents { let dict = textStyles.map { style -> [String: Any] in let type: String = style.fontStyle?.textStyleName ?? "" let textCase: String = { - switch style.textCase { - case .lowercased: - return "lowercase" - case .uppercased: - return "uppercase" - case .original: - return "original" + switch fontSystem { + case .swiftUI: + switch style.textCase { + case .lowercased: + return "lowercase" + case .uppercased: + return "uppercase" + case .original: + return "original" + } + case .uiKit: + return style.textCase.rawValue } }() return [ @@ -99,15 +121,15 @@ final public class XcodeTypographyExporter: XcodeExporterBase { ] } let env = makeEnvironment(templatesPath: output.templatesPath) - let contents = try env.renderTemplate(name: "TextStyle+extension.swift.stencil", context: ["styles": dict]) + let contents = try env.renderTemplate(name: fontSystem.styleExtensionStencilFile, context: ["styles": dict]) let textStylesSwiftExtension = try makeFileContents(for: contents, url: textStyleExtensionURL) return textStylesSwiftExtension } - private func makeTextStyle(textStylesDirectory: URL) throws -> FileContents { + private func makeStyle(fontSystem: FontSystem, directory: URL) throws -> FileContents { let env = makeEnvironment(templatesPath: output.templatesPath) - let textStyleSwiftContents = try env.renderTemplate(name: "TextStyle.swift.stencil") - return try makeFileContents(for: textStyleSwiftContents, directory: textStylesDirectory, file: URL(string: "TextStyle.swift")!) + let textStyleSwiftContents = try env.renderTemplate(name: fontSystem.styleStencilFile) + return try makeFileContents(for: textStyleSwiftContents, directory: directory, file: URL(string: fontSystem.styleFile)!) } } diff --git a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift index 314c27e9..d982c3e9 100644 --- a/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift +++ b/Tests/XcodeExportTests/XcodeTypographyExporterTests.swift @@ -26,10 +26,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( fontExtensionURL: URL(string: "~/UIFont+extension.swift")! ) - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() + let styleUrls = XcodeTypographyOutput.StyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) let output = XcodeTypographyOutput(urls: urls) let exporter = XcodeTypographyExporter(output: output) @@ -110,10 +110,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( fontExtensionURL: URL(string: "~/UIFont+extension.swift")! ) - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() + let styleUrls = XcodeTypographyOutput.StyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) let output = XcodeTypographyOutput( urls: urls, @@ -197,10 +197,10 @@ final class XcodeTypographyExporterTests: XCTestCase { let fontUrls = XcodeTypographyOutput.FontURLs( swiftUIFontExtensionURL: URL(string: "~/Font+extension.swift")! ) - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs() + let styleUrls = XcodeTypographyOutput.StyleURLs() let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) let output = XcodeTypographyOutput(urls: urls) let exporter = XcodeTypographyExporter(output: output) @@ -269,17 +269,17 @@ final class XcodeTypographyExporterTests: XCTestCase { func testExportStyleExtensions() throws { let fontUrls = XcodeTypographyOutput.FontURLs() - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( - textStylesDirectory: URL(string: "~/")!, - textStyleExtensionsURL: URL(string: "~/TextStyle+extension.swift")! + let styleUrls = XcodeTypographyOutput.StyleURLs( + directory: URL(string: "~/")!, + extensionsURL: URL(string: "~/TextStyle+extension.swift")! ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) let output = XcodeTypographyOutput( urls: urls, - generateTextStyles: true + generateStyles: true ) let exporter = XcodeTypographyExporter(output: output) @@ -549,16 +549,16 @@ final class XcodeTypographyExporterTests: XCTestCase { func testExportLabel() throws { let fontUrls = XcodeTypographyOutput.FontURLs() - let textStyleUrls = XcodeTypographyOutput.TextStyleURLs( - textStylesDirectory: URL(string: "~/")! + let styleUrls = XcodeTypographyOutput.StyleURLs( + directory: URL(string: "~/")! ) let urls = XcodeTypographyOutput.URLs( fonts: fontUrls, - textStyles: textStyleUrls + styles: styleUrls ) let output = XcodeTypographyOutput( urls: urls, - generateTextStyles: true + generateStyles: true ) let exporter = XcodeTypographyExporter(output: output)