Swiftgen: XCAssets: SwiftUI support

Created on 9 Dec 2019  Â·  8Comments  Â·  Source: SwiftGen/SwiftGen

Hey,

Is there a plan to add SwiftUI templates? Right now, all the templates rely on UIKit right now.

If not, I can help with implementing this 🙂

Probably something like swiftUI-5 to distinguish between UIKit and SwiftUI while still being able to differentiate between the Swift versions? Would love to know what you think, thanks.

enhancement

Most helpful comment

FYI: I wrote a custom template for generating SwiftUI code from xcassets.

It's possible to use images and colors from code generated from current swift5.stencil templates by SwiftGen. It's just not very pretty, brings us to String world again, and we can't provide guarantee about Bundle, from which this code was called:

Image(Asset.abc.name) // Image?

My custom template generates code similar to this:

internal extension Image {
// Assets.xcassets
  static var abc : Color { Image("abc", bundle: BundleToken.bundle) }
}

So now instead of String we get a non-optional Image object, that definitely is used from current bundle:

Image.abc // Image

It also handles namespaces, if forceProvidesNamespaces parameters was set to true(I personally don't recommend it):

internal extension Image {
// Assets.xcassets
  static var assets_folder_subfolder_abc : Color { Image("abc", bundle: BundleToken.bundle) }
}

Here's the gist: https://gist.github.com/DenTelezhkin/4bffa38b9e81db02a07e96c89fe7ddd5

If anyone is interested in making PR into main SwiftGen repo, please be my guest :) .

All 8 comments

I'd rather not create extra templates if it's not needed. Most templates support AppKit & UIKit at the same time. If we need to add another case/check in the template, I'd rather do that than create extra templates.

I personally haven't used swiftUI yet, so PRs for that are welcome.

So, I tried it and no extra check is enough from what I've gathered since both UIKit and SwiftUI can be imported on iOS 13.0. It would work if I could constrain the SwiftUI alternatives to codebases that are iOS 13.0 only but I do not think there is a way to do that.

There are two options then if we are to keep one template only:

  1. Passing option --swiftUI in generation command which would be then passed to .stencil files and create SwiftUI code instead of UIKit.
  2. Or preserving all variables like var color: AssetColorTypeAlias etc and then adding a complementary variable such as:
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
var swiftUIColor: Color

For option number two I sense there is a better name, but could not come up with one.

Thanks for the quick response BTW!

What is the actual type of a (for example) color asset in swiftUI? Color? UIColor? Do you have an example of what changes are needed to the output?

Definition of this variable for option nr. 2 would look something like this:

#if canImport(SwiftUI)
import SwiftUI
#endif

internal struct ColorAsset {
  internal fileprivate(set) var name: String

  internal var color: AssetColorTypeAlias {
    return AssetColorTypeAlias(asset: self)
  }
  @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
    internal var swiftUIColor: Color {
        return Color(self)
    }
}

Where you'd need to add a new initializer extension Color { init(ColorAsset) } in a similar fashion if we consider option number two.

But in a nutshell, the initializers of UIColor and Color are very similar:

UIColor(named: String, in: Bundle?, compatibleWith: UITraitCollection?)
Color(_ name: String, bundle: Bundle?)

The other types have also a similar level of resemblance.

FYI: I wrote a custom template for generating SwiftUI code from xcassets.

It's possible to use images and colors from code generated from current swift5.stencil templates by SwiftGen. It's just not very pretty, brings us to String world again, and we can't provide guarantee about Bundle, from which this code was called:

Image(Asset.abc.name) // Image?

My custom template generates code similar to this:

internal extension Image {
// Assets.xcassets
  static var abc : Color { Image("abc", bundle: BundleToken.bundle) }
}

So now instead of String we get a non-optional Image object, that definitely is used from current bundle:

Image.abc // Image

It also handles namespaces, if forceProvidesNamespaces parameters was set to true(I personally don't recommend it):

internal extension Image {
// Assets.xcassets
  static var assets_folder_subfolder_abc : Color { Image("abc", bundle: BundleToken.bundle) }
}

Here's the gist: https://gist.github.com/DenTelezhkin/4bffa38b9e81db02a07e96c89fe7ddd5

If anyone is interested in making PR into main SwiftGen repo, please be my guest :) .

I'm currently working on my first pure SwiftUI based app and created this custom template which works for my needs:

// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen

{% if catalogs %}
import SwiftUI
{% macro casesBlock assets %}
  {% for asset in assets %}
  {% if asset.items and asset.isNamespaced == "true" %}
  public enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
    {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
  }
  {% elif asset.items %}
  {% call casesBlock asset.items %}
  {% elif asset.type == "color" %}
  public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Color("{{asset.value}}")
  {% elif asset.type == "image" %}
  public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Image("{{asset.value}}")
  {% endif %}
  {% endfor %}
{% endmacro %}

/// Cases to reference for safely initializing colors, like so: `Clr.Fill.primary`
public enum Clr {
  {% for catalog in catalogs %}
  {% if catalog.name == "Colors" %}
  {% call casesBlock catalog.assets %}
  {% endif %}
  {% endfor %}
}

/// Cases to reference for safely initializing images, like so: `Img.Onboarding.header`
public enum Img {
  {% for catalog in catalogs %}
  {% if catalog.name == "Images" %}
  {% call casesBlock catalog.assets %}
  {% endif %}
  {% endfor %}
}
{% else %}
// No assets found
{% endif %}

Please note that this is targeted towards a separation of images and colors, where images would be placed into an Images.xcassets folder and colors into a Colors.xcassets folder.

Then, on the usage side, you can use colors and images like this:

var body: some View {
  VStack {
    Img.Onboarding.timeToOrganize
      .frame(maxHeight: 200)

    Text("Hello World")
      .foregroundColor(Clr.Content.onSurface)
  }
}

Note that I couldn't use the typealiases Image and Color as they are already used by SwiftUI. Instead I opted for the abbreviated type names Img and Clr to be kind of consistent with L10n for Strings.

I hope this helps someone to get started (feel free to adjust to your needs).

two years still no swiftui support?

So I started using the template that @Jeehut created. It's great for colors, however, there's a unfortunate problem with the Images and the way Apple decided to do accessibility. Image's are marked as decorative on init, and there does not seem to be a way for them to be marked as unimportant. My app has a bunch of background ones more so then even actionable images, so I ended out just having it create an enum of string names, so at least I have compile time checks on the Asset catalogues filenames.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jrmybrault picture jrmybrault  Â·  7Comments

kacper1703 picture kacper1703  Â·  3Comments

BastiaanAndriessen picture BastiaanAndriessen  Â·  4Comments

bengilroy picture bengilroy  Â·  3Comments

SummerHF picture SummerHF  Â·  3Comments