はじめに
xcassets
で管理しているImageやColorなどを型安全にアクセスするためのコードを自動生成するのに、SwiftGenをよく使用していたのですが、Swift6になってから自動生成されたコードからコンパイルエラーが返される様になったので、困ったということがありました。
そこで、Swift6の環境でSwiftGenを使用するために行なったことを記事で共有しようと思います。
SwiftGenとは?
swiftGenについては、以下githubリポジトリ、もしくは使い方も含めてわかりやすくまとまっている記事もありましたので、こちらをご参照ください。
Swift6環境でSwiftGenを使用すると起きるエラー内容
まず私は以下リソースを型安全にアクセスするコードの自動生成するためにSwiftGenを導入していました。
- Color
- Image
そこでswiftgen.yml
を以下のように定義していました。
input_dir: Resources
output_dir: ${DERIVED_SOURCES_DIR}
xcassets:
inputs:
- Color.xcassets
- Image.xcassets
outputs:
templateName: swift5
output: Assets.swift
params:
ForceProvidesNamespaces: true
ForceFileNameEnum: true
そうすると、以下のようなコードが自動生成されました。
自動生成コード
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
#if os(macOS)
import AppKit
#elseif os(iOS)
import UIKit
#elseif os(tvOS) || os(watchOS)
import UIKit
#endif
#if canImport(SwiftUI)
import SwiftUI
#endif
// Deprecated typealiases
@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0")
internal typealias AssetColorTypeAlias = ColorAsset.Color
@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0")
internal typealias AssetImageTypeAlias = ImageAsset.Image
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Asset Catalogs
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
internal enum Asset {
internal enum Color {
internal static let appPrimaryBackgroundColor = ColorAsset(name: "appPrimaryBackgroundColor")
}
internal enum Image {
internal static let graphCircle = ImageAsset(name: "graph_circle")
internal static let plusCircle = ImageAsset(name: "plus_circle")
}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
// MARK: - Implementation Details
internal final class ColorAsset {
internal fileprivate(set) var name: String
#if os(macOS)
internal typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
internal typealias Color = UIColor
#endif
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
internal private(set) lazy var color: Color = {
guard let color = Color(asset: self) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}()
#if os(iOS) || os(tvOS)
@available(iOS 11.0, tvOS 11.0, *)
internal func color(compatibleWith traitCollection: UITraitCollection) -> Color {
let bundle = BundleToken.bundle
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}
#endif
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
internal private(set) lazy var swiftUIColor: SwiftUI.Color = {
SwiftUI.Color(asset: self)
}()
#endif
fileprivate init(name: String) {
self.name = name
}
}
internal extension ColorAsset.Color {
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
convenience init?(asset: ColorAsset) {
let bundle = BundleToken.bundle
#if os(iOS) || os(tvOS)
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSColor.Name(asset.name), bundle: bundle)
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
internal extension SwiftUI.Color {
init(asset: ColorAsset) {
let bundle = BundleToken.bundle
self.init(asset.name, bundle: bundle)
}
}
#endif
internal struct ImageAsset {
internal fileprivate(set) var name: String
#if os(macOS)
internal typealias Image = NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
internal typealias Image = UIImage
#endif
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
internal var image: Image {
let bundle = BundleToken.bundle
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
let name = NSImage.Name(self.name)
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#if os(iOS) || os(tvOS)
@available(iOS 8.0, tvOS 9.0, *)
internal func image(compatibleWith traitCollection: UITraitCollection) -> Image {
let bundle = BundleToken.bundle
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#endif
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
internal var swiftUIImage: SwiftUI.Image {
SwiftUI.Image(asset: self)
}
#endif
}
internal extension ImageAsset.Image {
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
@available(macOS, deprecated,
message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
convenience init?(asset: ImageAsset) {
#if os(iOS) || os(tvOS)
let bundle = BundleToken.bundle
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSImage.Name(asset.name))
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
internal extension SwiftUI.Image {
init(asset: ImageAsset) {
let bundle = BundleToken.bundle
self.init(asset.name, bundle: bundle)
}
init(asset: ImageAsset, label: Text) {
let bundle = BundleToken.bundle
self.init(asset.name, bundle: bundle, label: label)
}
init(decorative asset: ImageAsset) {
let bundle = BundleToken.bundle
self.init(decorative: asset.name, bundle: bundle)
}
}
#endif
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
swift5系の環境では上記のコードでも問題なかったのですが、swift6の環境では以下の様なエラーが発生する様になりました。
Static property 'appPrimaryBackgroundColor' is not concurrency-safe because non-'Sendable' type 'ColorAsset' may have shared mutable state
ColorAsset
がSendable
でないためコンパイラに怒られているのがわかります。
対応方法
原因は、自動生成されるColorAsset
がSendable
に準拠していないことなので、自動生成するコードにSendable
プロトコルを付与できれば解決できそうです。
自動生成するコードをカスタマイズすることは可能で、templateのカスタマイズによって実現できます。
日本語の記事では以下が、templateのカスタマイズについてわかりやすく記載されていました。
本件と類似した問題がSwiftGenのissueにも上げられていて、そこでもtemplateのカスタマイズにてエラーの回避を行う提案がされていました。
具体的にコードベースでどの様に対応したのかを説明していきます。
まずswiftgen.yml
を以下の様に修正します。
input_dir: Resources
output_dir: ${DERIVED_SOURCES_DIR}
xcassets:
inputs:
- Color.xcassets
- Image.xcassets
outputs:
- templateName: swift5
+ templatePath: ./assets_custom_template.stencil
output: Assets.swift
params:
ForceProvidesNamespaces: true
ForceFileNameEnum: true
デフォルトで存在する自動生成コードの雛形であるswift5
というtemplateの指定から、templatePath
にてカスタマイズしたtemplateファイルのパスを指定します。
ちなみに、デフォルトのxcassets
のswift5
templateの内容は以下から確認できます。
https://github.com/SwiftGen/SwiftGen/blob/stable/Sources/SwiftGenCLI/templates/xcassets/swift5.stencil
次にtemplatePath
で指定したtemplateファイルを実装します。
assets_custom_template.stencil
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
{% macro hasValuesBlock assets filter %}
{%- for asset in assets -%}
{%- if asset.type == filter -%}
1
{%- elif asset.items -%}
{% call hasValuesBlock asset.items filter %}
{%- endif -%}
{%- endfor -%}
{% endmacro %}
{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
{% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %}
{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
{% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %}
{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
{% set hasARResourceGroup %}{% for catalog in catalogs %}{% call hasValuesBlock catalog.assets "arresourcegroup" %}{% endfor %}{% endset %}
{% set hasColor %}{% for catalog in catalogs %}{% call hasValuesBlock catalog.assets "color" %}{% endfor %}{% endset %}
{% set hasData %}{% for catalog in catalogs %}{% call hasValuesBlock catalog.assets "data" %}{% endfor %}{% endset %}
{% set hasImage %}{% for catalog in catalogs %}{% call hasValuesBlock catalog.assets "image" %}{% endfor %}{% endset %}
{% set hasSymbol %}{% for catalog in catalogs %}{% call hasValuesBlock catalog.assets "symbol" %}{% endfor %}{% endset %}
#if os(macOS)
import AppKit
#elseif os(iOS)
{% if hasARResourceGroup %}
import ARKit
{% endif %}
import UIKit
#elseif os(tvOS) || os(watchOS)
import UIKit
#endif
#if canImport(SwiftUI)
import SwiftUI
#endif
// Deprecated typealiases
{% if hasColor %}
@available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color
{% endif %}
{% if hasImage %}
@available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0")
{{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image
{% endif %}
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Asset Catalogs
{% macro enumBlock assets %}
{% call casesBlock assets %}
{% if param.allValues %}
// swiftlint:disable trailing_comma
{% set hasItems %}{% call hasValuesBlock assets "arresourcegroup" %}{% endset %}
{% if hasItems %}
@available(*, deprecated, message: "All values properties are now deprecated")
{{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [
{% filter indent:2," ",true %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %}
]
{% endif %}
{% set hasItems %}{% call hasValuesBlock assets "color" %}{% endset %}
{% if hasItems %}
@available(*, deprecated, message: "All values properties are now deprecated")
{{accessModifier}} static let allColors: [{{colorType}}] = [
{% filter indent:2," ",true %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
]
{% endif %}
{% set hasItems %}{% call hasValuesBlock assets "data" %}{% endset %}
{% if hasItems %}
@available(*, deprecated, message: "All values properties are now deprecated")
{{accessModifier}} static let allDataAssets: [{{dataType}}] = [
{% filter indent:2," ",true %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
]
{% endif %}
{% set hasItems %}{% call hasValuesBlock assets "image" %}{% endset %}
{% if hasItems %}
@available(*, deprecated, message: "All values properties are now deprecated")
{{accessModifier}} static let allImages: [{{imageType}}] = [
{% filter indent:2," ",true %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
]
{% endif %}
{% set hasItems %}{% call hasValuesBlock assets "symbol" %}{% endset %}
{% if hasItems %}
@available(*, deprecated, message: "All values properties are now deprecated")
{{accessModifier}} static let allSymbols: [{{symbolType}}] = [
{% filter indent:2," ",true %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %}
]
{% endif %}
// swiftlint:enable trailing_comma
{% endif %}
{% endmacro %}
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.type == "arresourcegroup" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}")
{% elif asset.type == "color" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
{% elif asset.type == "data" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
{% elif asset.type == "image" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
{% elif asset.type == "symbol" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}")
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2," ",true %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% endif %}
{% endfor %}
{% endmacro %}
{% macro allValuesBlock assets filter prefix %}
{% for asset in assets %}
{% if asset.type == filter %}
{{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
{% call allValuesBlock asset.items filter prefix2 %}
{% elif asset.items %}
{% call allValuesBlock asset.items filter prefix %}
{% endif %}
{% endfor %}
{% endmacro %}
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
{{accessModifier}} enum {{enumName}} {
{% if catalogs.count > 1 or param.forceFileNameEnum %}
{% for catalog in catalogs %}
{{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% if catalog.assets %}
{% filter indent:2," ",true %}{% call enumBlock catalog.assets %}{% endfilter %}
{% endif %}
}
{% endfor %}
{% else %}
{% call enumBlock catalogs.first.assets %}
{% endif %}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
// MARK: - Implementation Details
{% if hasARResourceGroup %}
{{accessModifier}} struct {{arResourceGroupType}}: @unchecked Sendable {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} var referenceImages: Set<ARReferenceImage> {
return ARReferenceImage.referenceImages(in: self)
}
@available(iOS 12.0, *)
{{accessModifier}} var referenceObjects: Set<ARReferenceObject> {
return ARReferenceObject.referenceObjects(in: self)
}
#endif
}
#if os(iOS)
@available(iOS 11.3, *)
{{accessModifier}} extension ARReferenceImage {
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
@available(iOS 12.0, *)
{{accessModifier}} extension ARReferenceObject {
static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set<ARReferenceObject> {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
}
}
#endif
{% endif %}
{% if hasColor %}
{{accessModifier}} final class {{colorType}}: @unchecked Sendable {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Color = UIColor
#endif
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
{{accessModifier}} private(set) lazy var color: Color = {
guard let color = Color(asset: self) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}()
#if os(iOS) || os(tvOS)
@available(iOS 11.0, tvOS 11.0, *)
{{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load color asset named \(name).")
}
return color
}
#endif
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} private(set) lazy var swiftUIColor: SwiftUI.Color = {
SwiftUI.Color(asset: self)
}()
#endif
fileprivate init(name: String) {
self.name = name
}
}
{{accessModifier}} extension {{colorType}}.Color {
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
convenience init?(asset: {{colorType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSColor.Name(asset.name), bundle: bundle)
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} extension SwiftUI.Color {
init(asset: {{colorType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(asset.name, bundle: bundle)
}
}
#endif
{% endif %}
{% if hasData %}
{{accessModifier}} struct {{dataType}} {
{{accessModifier}} fileprivate(set) var name: String
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} var data: NSDataAsset {
guard let data = NSDataAsset(asset: self) else {
fatalError("Unable to load data asset named \(name).")
}
return data
}
}
@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
{{accessModifier}} extension NSDataAsset {
convenience init?(asset: {{dataType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS) || os(watchOS)
self.init(name: asset.name, bundle: bundle)
#elseif os(macOS)
self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
#endif
}
}
{% endif %}
{% if hasImage %}
{{accessModifier}} struct {{imageType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Image = NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Image = UIImage
#endif
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
let name = NSImage.Name(self.name)
let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#if os(iOS) || os(tvOS)
@available(iOS 8.0, tvOS 9.0, *)
{{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
fatalError("Unable to load image asset named \(name).")
}
return result
}
#endif
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} var swiftUIImage: SwiftUI.Image {
SwiftUI.Image(asset: self)
}
#endif
}
{{accessModifier}} extension {{imageType}}.Image {
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
@available(macOS, deprecated,
message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property")
convenience init?(asset: {{imageType}}) {
#if os(iOS) || os(tvOS)
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(named: asset.name, in: bundle, compatibleWith: nil)
#elseif os(macOS)
self.init(named: NSImage.Name(asset.name))
#elseif os(watchOS)
self.init(named: asset.name)
#endif
}
}
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} extension SwiftUI.Image {
init(asset: {{imageType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(asset.name, bundle: bundle)
}
init(asset: {{imageType}}, label: Text) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(asset.name, bundle: bundle, label: label)
}
init(decorative asset: {{imageType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(decorative: asset.name, bundle: bundle)
}
}
#endif
{% endif %}
{% if hasSymbol %}
{{accessModifier}} struct {{symbolType}} {
{{accessModifier}} fileprivate(set) var name: String
#if os(iOS) || os(tvOS) || os(watchOS)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration
{{accessModifier}} typealias Image = UIImage
@available(iOS 12.0, tvOS 12.0, watchOS 5.0, *)
{{accessModifier}} var image: Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
#if os(iOS) || os(tvOS)
let image = Image(named: name, in: bundle, compatibleWith: nil)
#elseif os(watchOS)
let image = Image(named: name)
#endif
guard let result = image else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
{{accessModifier}} func image(with configuration: Configuration) -> Image {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
guard let result = Image(named: name, in: bundle, with: configuration) else {
fatalError("Unable to load symbol asset named \(name).")
}
return result
}
#endif
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} var swiftUIImage: SwiftUI.Image {
SwiftUI.Image(asset: self)
}
#endif
}
#if canImport(SwiftUI)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
{{accessModifier}} extension SwiftUI.Image {
init(asset: {{symbolType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(asset.name, bundle: bundle)
}
init(asset: {{symbolType}}, label: Text) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(asset.name, bundle: bundle, label: label)
}
init(decorative asset: {{symbolType}}) {
let bundle = {{param.bundle|default:"BundleToken.bundle"}}
self.init(decorative: asset.name, bundle: bundle)
}
}
#endif
{% endif %}
{% if not param.bundle %}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
{% endif %}
{% else %}
// No assets found
{% endif %}
ポイントは以下部分で@unchecked Sendable
をつけているところです。
{{accessModifier}} final class {{colorType}}: @unchecked Sendable {
{{accessModifier}} fileprivate(set) var name: String
#if os(macOS)
{{accessModifier}} typealias Color = NSColor
#elseif os(iOS) || os(tvOS) || os(watchOS)
{{accessModifier}} typealias Color = UIColor
#endif
やったこととしては、デフォルトのxcassets
のswift5
templateの内容をそのままコピペして、問題となっていたColorAsset
定義の部分でSendable
に準拠させる記述をしています。
Sendable
に準拠させるのに、@unchecked Sendable
で無理やり準拠させるのはいいのかという議論もあると思いますが、以下理由により問題ないと考えています。
変更可能なプロパティは
fileprivate
であり呼び出し側からアクセス出来ないため、実質変更される心配はない。
作成したassets_custom_template.stencil
をswiftgen.yml
のtemplateName
で指定したように、swiftgen.yml
の同階層に配置してビルドすればコンパイルエラーが起きないはずです。
おわり
上述していますが、SwiftGenのissueでも類似の問題は挙げられているので、そのうちswift6のデフォルトのtemplateもできるのかなーとは思っていますが、それまでの間は今回紹介した方法で回避する必要がありそうです。(ちょっと面倒ですが、、)
ご参考になる方いれば幸いです。
そのほか、誤りなどあればご指摘いただけますと幸いです!