【iOS/Xcode】SwiftGenでSwift6対応を行う方法!エラーや警告の解消

【iOS/Xcode】SwiftGenでSwift6対応を行う方法!エラーや警告の解消

この記事からわかること

  • SwiftSwiftGenとは?
  • 導入方法使い方
  • swiftgen.yml作成方法と記述すること
  • 実際に使用する流れ
  • 画像カラー参照方法
  • ローカライズした文字列参照方法
  • AssetL10nとは?

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

SwiftGen × Swift6で発生する警告エラー

SwiftGenはプロジェクト内で使用するリソース(画像やカラーなど)をタイプセーフに参照、管理するためのCLIツールです。タイプセーフに参照するためのSwiftコードファイルを自動生成してくれるのが特徴です。詳しい使い方は以下を参考にしてください。

中にはL10nColorAssetクラスなどが自動で定義されます。ただその自動生成されるクラスがSwift5までは問題なかったのですがSwift6モードにすると並行性を厳しくチェックするようになり、自動生成されたクラスがスレッドセーフではあることを明示的に定義できていないため警告が表示されるようになってしまいます。

解消方法

Swift6モードでも警告が発生しないようにするためにはSwiftGenのカスタムテンプレートという機能を使用します。SwiftGenではテンプレートを読み込んで自動生成するコードのルールを定義しています。通常はswift5structured-swift5などのデフォルトのテンプレートを使用していますが、ここにカスタムテンプレートを当てはめることでSwift6対応を行うことができます。


strings:
  inputs: Resources/Base.lproj
  outputs:
  # テンプレート指定
    - templateName: structured-swift5
      output: プロジェクト名/Generated/Strings.swift

xcassets:
  inputs:
    - プロジェクト名/Resources/Images.xcassets
    - プロジェクト名/Resources/Colors.xcassets
  outputs:
  # テンプレート指定
    - templateName: swift5
      output: プロジェクト名/Generated/Assets.swift

カスタムテンプレートの定義

SwiftGenのカスタムテンプレートは「Stencil」という構文を使用して記述する必要があります。今回Swift6対応するためのカスタムテンプレートはswift5テンプレートの中身をコピペして@unchecked Sendableに準拠するように変更している以下を使用します。これはissueで取り上げられていたものになります。

参考文献:GitHub:SwiftGen Errors on Swift 6

@unchecked Sendableはスレッドセーフのチェックから外れるようにするための記述ですがSwiftGenで生成されるコードは不変なので問題はありません。


// 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 %}

作成した「swiftgen_custom_template.stencil」ファイルを「swiftgen.yml」と同じ階層に設置します。「swiftgen.yml」内のtemplateNameで指定した箇所をtemplatePathに変更し作成したテンプレートファイルへのパスを指定してあげればOKです。


xcassets:
  inputs:
    - プロジェクト名/Resources/Images.xcassets
    - プロジェクト名/Resources/Colors.xcassets
  outputs:
  # テンプレート指定をPathに変更して指定
  - templatePath: ./swiftgen_custom_template.stencil
      output: プロジェクト名/Generated/Assets.swift

この状態で再度swiftgen config runコマンドを実行すればコードが再生成されSwift6の警告が出ないようになります。

$ swiftgen config run

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article