2021/12/17 追記
タイトルを変えました。
それっぽく名前を付けてみたってだけですがw
iOS Advent Calendar 2019も折り返しに来ました。担当の@417_72kiです。
protocol毎にextensionでコードブロックを分けるという有名(?)なhackがありますが、
今回はprotocol以外でも積極的にextensionで分けていこうぜ!っていう記事を書きました。
注意
どう呼べばいいか困ったものについて、この記事では以下のように定義しています。
-
definition block -> class/struct/enum宣言したブロック(
class Hoge {~}
で囲まれたブロック ) -
extension block -> extension宣言したブロック (
extension Hoge {~}
で囲まれたブロック )
以下、上記の言葉は太字で記述していきます。
ベタ書きされたコード
例としてこんなFatViewControllerを考えます(アーキテクチャの話は一旦無視します)。
ベタ書きされたFatViewController
class FatViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private var _items: [Item]?
var items: [Item] {
get { _items ?? [] }
set { _items = newValue }
}
var isEmpty: Bool { items.isEmpty }
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup() {
tableView.dataSource = self
tableView.delegate = self
}
private func doSomething(with item: Item) {
print(item.name)
}
func reloadView() {
tableView.reloadData()
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
doSomething(with: items[indexPath.row])
}
}
ステップ1: protocolを分割する
これについては参考記事があるのでここでの解説は割愛します。
protocol分割されたFatViewController
class FatViewController: UIViewController {
private var _items: [Item]?
var items: [Item] {
get { _items ?? [] }
set { _items = newValue }
}
var isEmpty: Bool { items.isEmpty }
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup() {
tableView.dataSource = self
tableView.delegate = self
}
private func doSomething(with item: Item) {
print(item.name)
}
func reloadView() {
tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
doSomething(with: items[indexPath.row])
}
}
ちなみに、protocol functionをextensionに切り出したら// MARK: - ~
を消している記事をよく見かけますが、筆者はあえて各extension blockに対して// MARK: - ~
を付けるようにしています。
理由は後述します。
ステップ2: functionを分割する
functionは基本的にextensionに切り出すことができます。
classにおけるoverride functionは例外で、definition blockにしか定義できないため、// MARK:
で区切ります。
*function*分割されたFatViewController
class FatViewController: UIViewController {
private var _items: [Item]?
var items: [Item] {
get { _items ?? [] }
set { _items = newValue }
}
var isEmpty: Bool { items.isEmpty }
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var button: UIButton!
// MARK: Life cycles
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
}
// MARK: - Functions
extension FatViewController {
private func setup() {
tableView.dataSource = self
tableView.delegate = self
}
private func doSomething(with item: Item) {
print(item.name)
}
func reloadView() {
tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
doSomething(with: items[indexPath.row])
}
}
ステップ3: computed propertyを分割する
propertyのうち、computed propertyはextensionに切り出すことができます。
stored propertyはdefinition blockにしか定義できないため、必要に応じて// MARK:
で区切ったりします。
computed property分割されたFatViewController
class FatViewController: UIViewController {
// MARK: Private properties
private var _items: [Item]?
// MARK: Outlets
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var button: UIButton!
// MARK: Life cycles
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
}
// MARK: - Computed properties
extension FatViewController {
var items: [Item] {
get { _items ?? [] }
set { _items = newValue }
}
var isEmpty: Bool { items.isEmpty }
}
// MARK: - Functions
extension FatViewController {
private func setup() {
tableView.dataSource = self
tableView.delegate = self
}
private func doSomething(with item: Item) {
print(item.name)
}
func reloadView() {
tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
doSomething(with: items[indexPath.row])
}
}
ステップ4: accessibleごとに分割する
extensionにもAccess Levelを設定することができます。
ブロック内のAccess Levelはブロック本体のAccess Level以下になります
(ただし、トップレベルのブロックにおけるprivate
はfileprivate
と同義になります)。
そこで、分割したextensionを更にAccess Levelごとに分割することで、ブロック単位でAccess Levelを設定できるとともにprivate
の付け忘れから開放されます。
accessibleごとに分割されたFatViewController
class FatViewController: UIViewController {
// MARK: Private properties
private var _items: [Item]?
// MARK: Outlets
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var button: UIButton!
// MARK: Life cycles
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
}
// MARK: - Computed properties
extension FatViewController {
var items: [Item] {
get { _items ?? [] }
set { _items = newValue }
}
var isEmpty: Bool { items.isEmpty }
}
// MARK: - Public Functions
extension FatViewController {
func reloadView() {
tableView.reloadData()
}
}
// MARK: - Private Functions
private extension FatViewController {
func setup() {
tableView.dataSource = self
tableView.delegate = self
}
func doSomething(with item: Item) {
print(item.name)
}
}
// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
doSomething(with: items[indexPath.row])
}
}
他にextensionに切り出せるもの
クラス定数(static let
)
FatViewController.swift
// MARK: - Constants
extension FatViewController {
static let initialItems: [Item] = [] // -> OK
let initialItems: [Item] = [] // -> NG
}
クラス定数(static let
)
// MARK: - Constants
extension FatViewController {
static let initialItems: [Item] = [] // -> OK
let initialItems: [Item] = [] // -> NG
}
同じ定数でもインスタンス定数の方はextensionに切り出せません。
不思議ですね(棒
inner class/struct/enum
FatViewController.swift
// MARK: - State
extension FatViewController {
enum State {
case hoge
case fuga
case foo
case bar
}
}
// MARK: -
extension FatViewController.State {
var isHoge: Bool { self == .hoge }
}
inner class/struct/enum
// MARK: - State
extension FatViewController {
enum State {
case hoge
case fuga
case foo
case bar
}
}
// MARK: -
extension FatViewController.State {
var isHoge: Bool { self == .hoge }
}
もちろんネストされた型に対してもextensionを貼ることができますし、
その中に更にネスト型を定義することもできます。
ブロックのネストを増やすことなく型のネストを増やせるので、複雑なJSONからCodableなstructを組み立てる時に重宝します。
ちなみに、BuildConfig.swiftという自作ツールで生成されるSwiftファイルもこの手法を使っています。
よかったら実際に使ってみて生成されたコードを見てみてください(宣伝
その他
実装する機会が減ってきたのでここでは触れませんが、convenience initializerもextensionで定義することができます。
// MARK:
とパンくずリストとMinimap
Xcodeのエディタ領域上部にあるパンくずリストで、ファイル名の次の要素を開くとDocument Itemsが表示されます。
(^+6
でも開きます)
// MARK: 見出し名
を使うことでこのDocument Itemsに見出しを付けることができます。
また、 MARK: - 見出し名
とすることで、見出しの前に区切り線を付けることができます。
更に、Xcode11で登場したminimapでは// MARK: 見出し名
を付けた所に見出しが表示されるようになります。
Document Itemsと同様、こちらも-
付きMARKにすると区切り線が付きます。
先述の、筆者が全ブロックにMARK: - 見出し名
を付ける理由がこれです。
見出しが付くおかげでコードの構造がパッと見で分かるようになりますね。
まとめ
extensionを活用してコードブロックを整理することで、各ブロックに役割を持たせる事ができます。
また、MARKコメントとminimapとの組み合わせでコードの見通しも良くなってDXも爆アゲです。
この1年ずっとこの手法を使っていますが今の所デメリットが見つかっていないので、
誰かこの手法で困ったことがあったら教えていただきたいです(※他の言語ではできないみたいなのは除く)。
それでは皆様、良いお年を!✋
参考
Using Swift Extensions The “Wrong” Way - Natasha The Robot(@NatashaTheRobot)
【Swift】Protocolごとにextensionで切り分けて実装するワケ(@ktanaka117)
[Xcode 8] Swiftのドキュメントコメントについての簡潔なまとめ(@y-some)
What's New in Xcode 11(@akatsuki174)