どうも。Reoです。
今回は、数年前からずっと使っていた R.swiftを使うのを辞めた話を書いていきます。
私が作ってきたiOSアプリでは、R.swiftはほぼ確実に使っていました。
しかし、Swift のバージョンやXcodeのバージョンが上がっていくにつれ、今ではそこまで必須のものではなくなってきたんじゃないかと思います。
R.swift とは
R.swift はリソースを使いやすくするためのライブラリです。
例えば、以下のようなリソースがあります。(READMEより)
let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")
これはR.swiftでは以下のように書けます。
let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")
なぜR.swiftをやめるのか
R.swift はとても便利です。ですが、この記事ではR.swiftを剥がす詳しいやり方について書いていきます。
R.swift を辞める一番の理由は、エラーが多いからです。
ブランチ切り替えですぐにエラーが発生する
ブランチを切り替えたあとにプロジェクトを開いた場合には、大抵 R がないというエラーが発生します。ブランチ切り替え等がない場合もよく発生します。
これはビルドすればすぐに消えるエラーですが、毎回目に見える形でエラー文が出てくるので、ストレスになります。(私は全く気にしないタイプだったんですが、チームメンバーそうではありませんでした。)
エクステンション機能で謎エラー
Application Extension 機能で動作せず、ビルドができなくなったのがが辞める決め手になりました。
Share Extension
や Capture Extension
といった新しいターゲットを追加すると、既存のターゲットも動作しなくなってしまいました。
解決は頑張ればできたのかな、とは思うんですが、前々から抱えていた利便性を上回るストレスより、辞めることを決意しました。
エラー自体は気になるので後で調べたいとは思っていますが、今は割愛。
R.swift は進化している
使っていた方はわかるかと思いますが、R.swift ではビルド時に Run Script より R.generated.swift
が自動で生成され、各リソースに割り当てされる値が生成されます。
昔はこの R.generated.swift
をプロジェクトに配置したり、Run Script を書く必要がありました。
現在は R.generated.swift
は生成されず、Build Tool Plugins を使って R.generated.swift
は見えないところにいます。
以前とは違い、Run Scripts にスクリプトを書くこともなく動かすことができます。Swift Package Manager から導入し、Build Tool Plugins を導入するだけで簡単に利用できるので、確実に進化しています。
Swift や Xcode も進化している
R.swift も進化していますが、Swift も進化しています。
R.swift では、文字列を指定することなく、安全にリソースを扱うことができます。 しかし、最近のSwiftでは同様に安全に画像や色にアクセスすることができます。
// 昔
let image1 = UIImage(named: "MyImage")
let color1 = UIColor(named: "MyColor")
// 最近
let image1 = UIImage(resouce: .myImage)
let color1 = UIColor(resource: .myColor)
UIImage.init(resource:) UIColor.init(resource:)
また、UIViewController
では nib
を特に指定しなくても対応する nib ファイルを自動で読み込んでくれるようになりました。
// 昔
let viewController = MyViewController(nibName: "MyViewController", bundle: nil)
// 最近 (これで自動で nib も読み込んでくれる)
let viewController = MyViewController()
いつからっていうのはちょっと面倒なので割愛。
また、実際にR.swiftを剥がす作業をしていて感じたのは、UITableView
や UICollectionView
における便利さでした。
現在では UITableView
はほぼオワコンになってしまい(UICollectionViewで同等の機能が出ているため)使うことはほぼありません。
また、UICollectionView を使う場合にも UICollectionViewCompositionalLayout
+ # UICollectionViewDiffableDataSource
を使うようになったため、tableView.register
や R.reuseIdentifier
の出番は無くなりました。
さらに、おそらくほとんどの新規のプロジェクトでは SwiftUI が使われるため、これらの出番はさらに減っています。
使っていたところ
R.swift を利用していたのは以下の部分でした。
- 画像 (
R.image
) - 色 (
R.color
) - IBファイル (
R.nib
) - UICollectionViewやUITableView (
R.reuseIdentifier
) - ローカライズした文字 (
R.string.localizable
) - フォント (
R.font
) - 音源ファイルやテキストファイル (
R.file
)
Storyboardは使わない派なので使っていないです。
置き換え方
主な置き換え型は以下の通りです。
項目 | 🟠R.swift | 🟢R.swiftはがし |
---|---|---|
画像 | R.image.hogeImage() |
UIImage(resource: .hogeImage) |
色 | R.color.lightPink() |
UIColor(resource: .lightPink) |
nib | R.nib.hogeView |
UINib(nibName: "HogeView", bundle: nil) |
UITableView UICollectionView |
R.reuseIdentifier.hogeCollectionViewCell |
HogeCollectionViewCell.className (クラス名じゃなくても全然ok) |
フォント | R.font.hogeBold() |
UIFont(name: "hogeBold.otf", size: 20) |
ローカライズ | R.string.localizable.hogeText() |
String(localized: "Hoge.Text", defaultValue: "Hoge") String Catalogs を使う |
音源ファイル | R.file.hogeMp3() |
Bundle.main.url(forResource: "Hoge", withExtension: "mp3")! |
テキストファイル | R.file.termsOfServiceTxt() |
Bundle.main.path(forResource: "TermsOfService", ofType: "txt")! |
文字列を打たなきゃいけない部分はいくつかあるので、 まだまだ R.swift の強みはありますよね。正直動作が安定していればまだまだ使いたいし...。
画像
R.swift
R.image.hogeImage()
R.swift はがし
UIImage(resource: .hogeImage)
標準でこれが実装されたのは本当に革命。
VSCodeで置換しました!
置換
VSCodeで検索 (多分Xcodeでも全然ok)
R\.image\.([a-zA-Z0-9_]+)\(\)!
置換
UIImage(resource: .$1)
色
R.swift
R.color.lightPink()
R.swift はがし
UIColor(resource: .lightPink)
これも本当に革命!
置換
これもVSCodeで置換しました。
検索
R\.color\.([a-zA-Z0-9_]+)\(\)!
置換
UIColor(resource: .$1)
ViewController系
R.swift
HogeViewController(nib: R.nib.hogeViewController)
R.swift はがし
HogeViewController()
// もしくは
HogeViewController(nibName: HogeViewController.nibName, bundle: nil)
import UIKit
extension UIViewController {
static var nibName: String {
String(describing: self)
}
}
extension で乗り切れます。
カスタムView系
R.swift
R.nib.hogeView
R.swift はがし
UINib(nibName: HogeView.nibName, bundle: nil)
import UIKit
extension UIView {
static var nibName: String {
String(describing: self)
}
}
これは結構便利だったと思います。
UITableView
R.swift
// セルの登録
tableView.register(R.nib.hogeTableViewCell)
// セルの再利用
let cell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.hogeTableViewCell, for: indexPath)!
R.swift はがし
// セルの登録
tableView.register(UINib(nibName: HogeTableViewCell.nibName, bundle: nil), forCellReuseIdentifier: HogeTableViewCell.className)
// セルの再利用
guard let cell = tableView.dequeueReusableCell(withIdentifier: HogeTableViewCell.className, for: indexPath) as? HogeTableViewCell else { fatalError("cellの設定が正しくありません。") }
import Foundation
extension NSObject {
class var className: String {
return String(describing: self)
}
var className: String {
return type(of: self).className
}
}
UITableViewが現役であれば、多分使い続けていたと思います。R.swift の一番の恩恵だったと個人的には思っています。
UICollectionView
R.swift
// セルの登録
collectionView.register(R.nib.hogeCollectionViewCell)
// セルの再利用
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.hogeCollectionViewCell, for: indexPath)!
R.swift はがし
// セルの登録
collectionView.register(UINib(nibName: HogeCollectionViewCell.nibName, bundle: nil), forCellReuseIdentifier: HogeCollectionViewCell.className)
// セルの再利用
guard let cell = collectionView.dequeueReusableCell(withIdentifier: HogeCollectionViewCell.className, for: indexPath) as? HogeCollectionViewCell else { fatalError("cellの設定が正しくありません。") }
import Foundation
extension NSObject {
class var className: String {
return String(describing: self)
}
var className: String {
return type(of: self).className
}
}
UICollecitonView
は最近では UICollecionViewDiffableDataSouce
でしか使っていません。
UITableView
は使っていないし、UICollectionView
は今もめちゃくちゃ使っているけれど、ReuseIdentifier を使うことはありません。diffableDataSource 自体はめちゃ苦戦しています...。
フォント
R.swift
R.font.hogeBold(size: 20)
R.swift はがし
UIFont(name: "hogeBold.otf", size: 20)
実際のプロジェクトでは使っている箇所が多いので、こんな感じに置き換えました。
enum FontResource {
static let hogeBold = "hogeBold.otf"
}
UIFont(name: FontResource.hogeBold, size: 10)
コード上のテキスト
R.swift をはがし、さらに Localizable.strings
から String Catalogs ( Localizable.xcstrings
) に移行しました。
Localizalbe.strings
// en
"Hoge.title" = "Hoge";
// ja
"Hoge.title" = "ほげ";
R.swift
R.string.localizable.hogeTitle()
R.swift はがし
String(localized: "Hoge.title", defaultValue: "Hoge")
日本語は String Catalogs より追加しました。。
xib上のテキスト
ここからは R.swift は関係ないですが、String Catalogs への移行について書いていきます。
String Catalogs については以下の記事で勉強しました。
String Catalogsの紹介 - WWDC23 - ビデオ - Apple Developer String Catalogsの概要と妥当な運用方針
Localizable.strings
これまではEasy XIB and Storyboard Localizationの記事のやり方で xib
上の xibLocKey
に Localizable.strings
のキーを設定する方法を使っていた。
String Catalogs を使う
ローカライズを行いたい .xib
ファイルを開きます。
Show the File inspector
> Localization
> Localize...
を選択。
Use String Catalog
にチェックを入れて Localize
を行います。
次のようなファイルが生成されました。
HomeViewController (Strings)
(HomeViewController.xcstrings
) を開きます。
HomeViewController.xib
内のテキストが自動で抽出されるので、ローカライズを行います。
自動抽出されたラベルのローカライズを行いたくない場合には、 Mark as "Don't Translate"
を選択しておきました。
必要な箇所のみローカライズを行い ✅ 状態にします。
翻訳が必要ないところは、Don't translate
を使うことで、翻訳の進捗を100%にしておくことができます。
String Catalogs 上で手動の部分をなくす
String Catalogs には、手動でも値を追加することができます。
しかし、手動追加では定義している部分に飛ぶなどの機能が使えなくなるので、結構微妙だなぁとおもって、手動追加の部分は無くすようにしました。
代わりに各xib上での定義を行っています。
手動で追加した項目ついては String Catalogs 上で ➖
ボタンより削除することができます。反対にコード上で追加した項目については削除することはできません。
コード上で追加した項目やxibと1:1で定義されている場合には、Key の部分より定義したファイルに飛ぶことができるので、非常に便利な機能だと思います。
悩みどころ
全然本題とは離れてしまうんですが、文字列のローカライズを行うにあたって、めちゃくちゃ悩んでいる部分があります。
テキストのローカライズは String(localized:defalutValue:)
を使っています。
let title = String(localized: "MyViewController.tile", defalutValue: "Title")
これを、ファイル上の見やすさを優先して以下のように書くとします。
let title = String(
localized: "MyViewController.title",
defalutValue: "Title"
)
これは一件見やすいですが、検索をかけたときに String(localized:
に引っ掛からなくなります。いつか置換することを考えると非常に面倒です。
今回も、R.swift を剥がすために正規表現での置き換えを利用しました。 しかし、改行を用いていると、検索では出て来ず正規表現でも対象外になってしまいます。
あなたならどうしますか?
自分は非常に迷って、もう直すことはないだろう!もしくは、改行込みの正規表現で置き換えればなんとかなるはず!と思ってこのままにしています。(最初に置き換えたのが改行ありだったから面倒だったなんて(ry
かなり迷っています。検索で出ないのは正直困るけど、defaultValue を含めると長ったらしくて仕方ないのよねぇ...
悩みどころ②
Localizable.xcstrings
を1つにまとめることができたら、1つにまとめるほうが漏れがなくなっていいなじゃないか?ということです。
ChatGPTに聞いたところ、規模の小さい場合ではバラバラでもよく、規模が大きいほど まとめた方がいいのではないかということでした。
自分的には、規模が多くなるほど、多くの人が別のファイルを触ることが多くなるので、Localizable.xcstrings
でコンフリクトすることが多くなってしまうのではないかとも思います。
現在は大規模プロジェクトを触ることがないのでわかりませんが、まとめることができるのであればまとめたほうがいいのか...というのが悩みどころです。
まとめるには xib と紐づいている ObjectKey をどうするか問題があって、そこが解決できていないので、結局 xib と String Catalogs は 1:1 の状態にしています。
あんまりやっている人もいないので、もし自分と違う方法でやっているよっていう場合は教えてくれると嬉しいです。(ブログにコメントフォームもないのでごめんなさい...DMかリプでもくれると嬉しい...)
まぁでもそもそも xib っていうものをもうみんな使わないのかもな...
おわりに
置き換え手順はシラフでメモ書きしたんですが、ブログ記事本体はゲロゲロ酔った状態で書いてしまったので、誤字脱字があったらごめんなさい。(後日ちゃんと確認します!)
数年前から、R.swift は自分にとっては全てのプロジェクトに追加するほどの定番ライブラリでした。
そんなライブラリを手放すことは少し寂しくあります。いつもお世話になりました。ありがとうございます...
SwiftやXcodeも時代と共に進化していき、足りない機能を補ってくれていたライブラリは、いつしか必要がなくなってしまうこともあるでしょう。
今までありがとうということぐらいしかできないけれど、ありがとう...
それでは。
コメントはありません。
現在コメントフォームは工事中です。