Skip to content

Public asset symbol constants in Swift packages

Xcode auto-generates typed asset constants from .xcassets catalogs, letting you write Color(.mintGreen) / Image(.logoMark) (backed by ColorResource / ImageResource). Unfortunately, currently there's no solution for making such assets public in a package like AppCommons that wants to provide assets for other packages to use. The constants are always generated internal, so other packages can't see them. There's no public-access option, and the generator can't be turned off from Package.swift.

Workaround

Convention: inside each .xcassets catalog, put assets meant for cross-package use into a Public/ subfolder:

With the little help of a Ruby script, you can auto-generate constants like this:

swift
Image(.AppCommons.logoMark)
Color(.AppCommons.mintGreen)

The constants are nested under an enum named after the containing package to avoid clashing with Xcode's own generated (internal) symbols.

The script scans the project for **/*.xcassets/Public/ and writes a <PackageName>+Generated.swift next to each catalog:

swift
public extension ImageResource {
    enum AppCommons {
        public static let logoMark = ImageResource.logoMark
    }
}
public extension ColorResource {
    enum AppCommons {
        public static let skyBlue = ColorResource.skyBlue
    }
}

The script

rb
# Scans a root path for `<name>.xcassets/Public/` folders and generates a
# public asset-constants file next to each catalog. The nested enum name is
# derived from the SPM package (the /Sources/<Name>/ path segment).
# Xcode's own asset symbols are always `internal` and can't be turned off
# inside an SPM package, so this is only for cross-package (public) access.
# See https://forums.swift.org/t/xcode15-generated-imageresource-with-public-access/67293
#
# Usage: ruby gen-app-assets.rb <root>

require 'pathname'

root = ARGV[0]
abort "usage: gen-app-assets.rb <root>" if root.nil?

def ident(name)
  parts = name.split(/[_\-.\s]+/).reject(&:empty?)
  first, *rest = parts
  first[0].downcase + first[1..].to_s + rest.map { |p| p[0].upcase + p[1..].to_s }.join
end

Dir.glob(File.join(root, '**', '*.xcassets', 'Public')).sort.each do |public_dir|
  public_dir = Pathname.new(public_dir)
  catalog = public_dir.parent
  ns = catalog.to_s.match(%r{/Sources/([^/]+)/})&.captures&.first
  if ns.nil?
    warn "skip #{catalog}: cannot derive namespace (expected .../Sources/<Name>/...)"
    next
  end

  images, colors = [], []
  Dir.glob(public_dir + '**/*').sort.each do |p|
    base = File.basename(p)
    case File.extname(base)
    when '.imageset' then images << File.basename(base, '.imageset')
    when '.colorset' then colors << File.basename(base, '.colorset')
    end
  end

  lines = []
  lines << '// This file is auto-generated by tools/gen-app-assets. Do not edit.'
  lines << '// Re-exports Xcode-generated internal asset constants publicly.'
  lines << ''
  lines << 'import DeveloperToolsSupport'
  lines << ''
  lines << 'public extension ImageResource {'
  lines << "    enum #{ns} {"
  images.each { |n| id = ident(n); lines << "        public static let #{id} = ImageResource.#{id}" }
  lines << '    }'
  lines << '}'
  lines << ''
  lines << 'public extension ColorResource {'
  lines << "    enum #{ns} {"
  colors.each { |n| id = ident(n); lines << "        public static let #{id} = ColorResource.#{id}" }
  lines << '    }'
  lines << '}'
  lines << ''

  out = catalog.parent + "#{ns}+Generated.swift"
  out.write(lines.join("\n"))
  puts "Wrote #{out} (enum #{ns}, #{images.size} images, #{colors.size} colors)"
end

Rerun after adding or renaming assets, e.g. as a just recipe:

gen-app-assets:
    ruby tools/gen-app-assets/gen-app-assets.rb Packages

Feedback

FB18574624: Generating Asset symbol constants with public access in a Swift package

I'm using an Asset catalog in a Swift package to define colors that are to be used in other Swift packages.

Currently there seems no way to make the extensions to „DeveloperToolsSupport.ColorResource" (and the like) public, nor to disable the generation at all.

This is cumbersome. As a workaround, I write the extension to Color/UIColor myself, but then these colors are to be used differently when they come from a package (Color.skyBlue) vs. when they come from the local package (Color(.skyBlue) — I'd prefer the ColorResource style).

Maybe a 2nd option „Public Asset Symbols" next to „Asset Catalog » Asset Symbols" in the .xcassets that would apply both to ColorResource / Color extensions?

See also: ↗ Swift Forums discussion