1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftGenで自動生成したColor定義をSwift6環境で使用する方法

Posted at

はじめに

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

ColorAssetSendableでないためコンパイラに怒られているのがわかります。

対応方法

原因は、自動生成されるColorAssetSendableに準拠していないことなので、自動生成するコードに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ファイルのパスを指定します。

ちなみに、デフォルトのxcassetsswift5templateの内容は以下から確認できます。
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

やったこととしては、デフォルトのxcassetsswift5templateの内容をそのままコピペして、問題となっていたColorAsset定義の部分でSendableに準拠させる記述をしています。

Sendableに準拠させるのに、@unchecked Sendableで無理やり準拠させるのはいいのかという議論もあると思いますが、以下理由により問題ないと考えています。

変更可能なプロパティはfileprivateであり呼び出し側からアクセス出来ないため、実質変更される心配はない。

作成したassets_custom_template.stencilswiftgen.ymltemplateNameで指定したように、swiftgen.ymlの同階層に配置してビルドすればコンパイルエラーが起きないはずです。

おわり

上述していますが、SwiftGenのissueでも類似の問題は挙げられているので、そのうちswift6のデフォルトのtemplateもできるのかなーとは思っていますが、それまでの間は今回紹介した方法で回避する必要がありそうです。(ちょっと面倒ですが、、)
ご参考になる方いれば幸いです。
そのほか、誤りなどあればご指摘いただけますと幸いです!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?