Swift Package内のリソースにアクセスする便利な方法

## はじめに

本記事は [Swift/Kotlin愛好会 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/love_swift_kotlin) の14日目の記事です。
空いていたので埋めました。

Swift Package内のリソースにアクセスするには `bundle:` に `Bundle.main` でなく `Bundle.module` を指定する必要があります。
毎回指定するのが手間なので、私は便利メソッドを実装し、さらに __リソースへのアクセスを1つのSwiftファイルにまとめています__ 。
その方法を紹介します。

## 環境

- OS：macOS Big Sur 11.6
- Swift：5.5.2
- Xcode：13.2.1 (13C100)

## リソースの格納

Swift Packageにおいて、リソースのほとんどは `Sources/{target name}/Resources/` フォルダに格納すれば自動的にリソースと認識します。

## リソースにアクセスする便利な方法

私は `R.swift` というファイルを作成し、Androidに合わせて `R.` で __静的にリソースへアクセスできる__ ようにしています。

アクセス修飾子は基本的に `internal` （何も付けない）にしておき、 __target外に公開するリソースのみ`public` にする__ のがいいと思います。

### NSLocalizedString

`Localizable.strings` からローカライズ文字列を取得します。

`createNSLocalizedString(_:)` メソッドを用意して、簡単にローカライズ文字列を生成できるようにしています。
`NSLocalizedString` の実態は `String` なのに注意です。

ローカライズ文字列は `static let` で静的に保持しています。
__変数名はできる限りリソース名に合わせるとわかりやすい__ です。

```swift:R.swift
enum R {
    enum LocalizedString {

        // MARK: Internal Stored Type Properties

        // !!!: ここにローカライズ文字列へのアクセスを追加する
        static let contactUs = nsLocalizedString("Contact us")
        static let version = nsLocalizedString("Version")

        // MARK: Other Private Type Methods

        private static func nsLocalizedString(_ key: String) -> String {
            NSLocalizedString(key, bundle: .module, comment: key)
        }
    }
}
```

呼び出し方は以下の通りです。

```swift
label.text = R.LocalizedString.contactUs
```

### UIColor

`*.xcassets` から `UIColor` を取得します。

`NSLocalizedString` とほぼ同様なので、詳細は省略します。

```swift:R.swift
enum R {
    enum Color {

        // MARK: Internal Stored Type Properties

        // !!!: ここに色へのアクセスを追加する
        static let labelBackground = uiColor(named: "LabelBackground")

        // MARK: Other Private Type Methods

        private static func uiColor(named name: String) -> UIColor {
            guard let color = UIColor(named: name, in: .module, compatibleWith: nil) else {
                fatalError("Fail to load '\(name)' color.")
            }
            return color
        }
    }
}
```

呼び出し方は以下の通りです。

```swift
label.backgroundColor = R.Color.labelBackground
```

### UIImage

`*.xcassets` から `UIImage` を取得します。

`UIColor` とほぼ同様なので、詳細は省略します。

```swift:R.swift
enum R {
    enum Image {

        // MARK: Internal Stored Type Properties

        // !!!: ここに画像へのアクセスを追加する
        static let uhooiIcon = uiImage(named: "Uhooi")

        // MARK: Other Private Type Methods

        private static func uiImage(named name: String) -> UIImage {
            guard let image = UIImage(named: name, in: .module, with: nil) else {
                fatalError("Fail to load '\(name)' image.")
            }
            return image
        }
    }
}
```

呼び出し方は以下の通りです。

```swift
imageView.image = R.Image.uhooiIcon
```

### UIStoryboard

`*.storyboard` から対象の `UIViewController` を取得します。

私は `UIStoryboard` を直接使うことがないので、 `instantiateInitialViewController(_:)` メソッドを用意して対象の `UIViewController` を取得するようにしています。

```swift:R.swift
enum R {
    enum Storyboard {

        // MARK: Enums

        // !!!: ここにストーリーボードへのアクセスを追加する
        enum MonsterList {
            static func instantiateInitialViewController() -> MonsterListViewController {
                Storyboard.instantiateInitialViewController(named: "MonsterList")
            }
        }
        enum MonsterDetail {
            static func instantiateInitialViewController() -> MonsterDetailViewController {
                Storyboard.instantiateInitialViewController(named: "MonsterDetail")
            }
        }

        // MARK: Other Private Type Methods

        private static func instantiateInitialViewController<T: UIViewController>(named name: String) -> T {
            guard let vc = uiStoryboard(name: name).instantiateInitialViewController() as? T else {
                fatalError("Fail to load \(T.self) from '\(name)' Storyboard.")
            }
            return vc
        }

        private static func uiStoryboard(name: String) -> UIStoryboard {
            UIStoryboard(name: name, bundle: .module)
        }
    }
}
```

呼び出し方は以下の通りです。

```swift
let vc = R.Storyboard.MonsterList.instantiateInitialViewController()
```

### UINib

`*.xib` から `UINib` を取得します。

他とほぼ同様なので、詳細は省略します。

```swift:R.swift
enum R {
    enum Nib {

        // MARK: Internal Stored Type Properties

        // !!!: ここにジブへのアクセスを追加する
        static let monsterCollectionViewCell = uiNib(name: "MonsterCollectionViewCell")

        // MARK: Other Private Type Methods

        private static func uiNib(name: String) -> UINib {
            UINib(nibName: name, bundle: .module)
        }
    }
}
```

呼び出し方は以下の通りです。

```swift
let monsterCellRegistration = UICollectionView.CellRegistration<MonsterCollectionViewCell, MonsterItem>(
    cellNib: R.Nib.monsterCollectionViewCell) { cell, _, monster in
    cell.setupWith(name: monster.name, iconUrl: monster.iconUrl, elevation: 1.0)
}
```

## おわりに

これでSwift Package内のリソースへ簡単にアクセスできます :relaxed: 

ただ手動で実装するのは手間なので、 __ツールを使って自動で生成したい__ です。
[R.swift](https://github.com/mac-cain13/R.swift) ではできなさそうでした。
試したことないですが、 [SwiftGen](https://github.com/SwiftGen/SwiftGen) だとテンプレートを自作できるのでできそうです。

https://twitter.com/griffin_stewie/status/1484121945738076161?s=20

また、みなさんのSwift Package内のリソースへのアクセス方法も教えていただけると嬉しいです。

以上 [Swift/Kotlin愛好会 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/love_swift_kotlin) の14日目の記事でした。

## 参考リンク

- [Bundling Resources with a Swift Package | Apple Developer Documentation](https://developer.apple.com/documentation/swift_packages/bundling_resources_with_a_swift_package)
- [Swift Package centered project - Build and Practice - Speaker Deck](https://speakerdeck.com/d_date/swift-package-centered-project-build-and-practice)
- [UhooiPicBook/Sources/AppModule/R.swift](https://github.com/uhooi/UhooiPicBook/blob/3b11c2d467e9bb770edc48f9656710dfd905a9c1/Sources/AppModule/R.swift)
- [UhooiPicBook/Sources/MonsterWidgets/R.swift](https://github.com/uhooi/UhooiPicBook/blob/3b11c2d467e9bb770edc48f9656710dfd905a9c1/Sources/MonsterWidgets/R.swift)