242
187

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ワンランク上のSwiftを書くための厳選記法10選

Last updated at Posted at 2018-12-19

はじめに

:tada: Swift Advent Calendar 2018の20日目を担当させていただきます @ruwatana です。

主流となっているモダンな言語は多様な概念を採用しており、さまざまな記法を使って十人十色のコードを書くことが可能となっています。
今回は、みんな大好きSwiftの記法に注目し、自分も普段から取り入れているオシャレでスマートないわゆるSwiftyな記法を実際の使用例やなぜそう書くと良いのかといった理由とともに厳選してみました。
設計方針などはある程度学習が必要となりますが、今回紹介するのは記法なので今から実践することが可能です:point_left:

Type Omitting (型省略)編

Swiftには強力な**Type Inference(型推論)**がありますが、これを用いて非常に簡潔に書くことが可能となっています。
こうした型省略はよく用いると思いますが、応用するとさらに強力です。

① Enum

まずは、enumを使った型省略の例です。
これは基本中の基本ですね。

enumの型省略の例
enum Pattern {
    case hoge, fuga
}

let pattern = Pattern.hoge // 型推論によって型を定義しなくてもPattern型となる
let pattern: Pattern = .hoge // Pattern型として定義されてるのでPattern.hogeと書かなくて良い

// switch構文でも省略可能
switch pattern {
case .hoge: print("hoge")
case .fuga: print("fuga")
}

② Property, Function

enum以外にも型省略は使えます。
次は、自分自身の型を返すstaticなpropertyやfunctionにて型省略をする例です。
functionに型省略を使ったりしてるコードを見ると個人的には「おっ!?」となります:sunglasses:

自分自身の型を返すpropertyやfunctionでの使用例
let label = UILabel()
label.backgroundColor = .red // backgroundColorはUIColor?型なので型省略可能
label.font = .systemFont(ofSize: 10.0) // UIFont型を返すfunctionも型省略可能

③ Initializer

上記の応用です。initも自分自身を返すstatic functionといえるので適合可能です。
とっても長いTypeを用いたinitializerは横に長くなってしまい見にくいです。
そんな時は、こんな風にも書けるので覚えておくと良いかもしれません。

initializerを型省略した例
class ViewController: UIViewController {
    private var edgePanGesture: UIScreenEdgePanGestureRecognizer? //長いClass名
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // △: とっても長い
        edgePanGesture = UIScreenEdgePanGestureRecognizertarget: self, action:: #selector(handle)

        // ○: .initだと非常に簡潔にかける
        edgePanGesture = .init(target: self, action: #selector(handle))

        view.addGestureRecognizer(edgePanGesture!)
    }
    
    @objc func handle() {
        print("recognized")
    }
}

型省略すると該当の行を見ただけでは型が想像しにくい?:thinking:

自分は、冗長な書き方はせずに極力短くかつ明瞭に書いてある方が、よりSwiftyなのではないかなと思っています。
型省略してしまうことで、該当の行を読んだだけではかえって型の想像がつきにくい場合があるかもしれませんが、自分は下記の機能を有効にしているため、あまり気になりません。

カスタムカラースキームでシンタックスハイライトの色分けを細分化

こんな感じで、property/method/enum caseを標準ライブラリとカスタム定義に分けて全てを異なるカラーで定義するようにしたカラースキームを独自に作成して使っています。
これによって省略記法を使っても、一目でどんな型か想像がつきやすくしています。
image.png
カラースキームの設定についての詳細は、下記の過去記事をご参考ください:bow:
Xcode テーマのおすすめ設定〜自作してシンタックスハイライトをいい感じにする〜

トラックパッドの調べる機能で型を参照する

トラックパッドの調べる&データ検出機能を有効にし、該当の行のpropertyやmethod上で操作を行うと、簡単に型をしらべることができます。
自分は、3本指タップで調べる機能を使用するようにしています。

システム環境設定 > トラックパッド > ポイントとクリック > 調べる&データ検出をチェック :white_check_mark:
image.png

先ほどの例で使用するとこんな感じです。
modalTransitionStyleの上にマウスカーソルをfocusし3本指タップすると型の情報がポップアップで表示できるので覚えておくと便利です。
image.png

また別の方法としては、テキストカーソルを型を調べたいターゲットに合わせた状態で、Xcode右ペインのQuick Helpにも表示できます。
image.png

Extension編

Extensionは、既存のclassを拡張するためにも有効な手段ですが、可読性の面でも一役買ってくれます。

④ Protocol実装をExtensionで分ける

Protocol実装をExtensionにて分けることは、コードの可読性を上げる上で非常に有効な手段となります。
Protocolの定義はclass/structなどの最初で定義することができますが、これではいくつかの問題が生じます。最初に定義した場合の例を見ていきましょう。

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
}

まず、最初の定義の行が非常に長くなってしまいました。
さらに、すべてのメソッドが羅列されているので、このメソッドはどのProtocolメソッドなのかがわからないといった問題が発生します。

そこで、ExtensionにてそれぞれのProtocolへの準拠と実装を分けてみます。

class ViewController: UIViewController { }

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
}

どうでしょうか。
Class定義の箇所もクラス継承のみとなり、短く明瞭になりました。
Protocolごとにextensionで区切られたため、一目でProtocolメソッドを実装しているということがわかるようにもなりました。

この記法は、もちろんProtocolだけではなく、単純な処理のカテゴライズにも使うことができます。
可読性を上げるためにextensionに(場合によっては別ファイルに)切り出すことを意識してみると、良いと思います:ok_woman:

ただし、下記の注意点もあるので一緒に覚えておくと良いでしょう。

  • Extensionに切り出すことでアクセス修飾子の公開範囲が広がってしまうケースがある
  • Extensionではstored propertyを定義することができない

Optional編

SwiftではOptionalは?というリテラルで表現されています。
if letなどのOptional Bindingを駆使してunwrapして取り扱うことが多いと思いますが、ここではOptionalのままでも簡潔に扱う方法を紹介します。

⑤ Optional Chaining

optional型のプロパティにアクセスしつつ、unwrapできればそれ以降のロジックが実行され、nilの場合は実行されないという書き方が?一つで表現できます。
これはXcode上で勝手に補完されたりするのでよく目にする機会があると思います。

オプショナルチェーンの使用例
class ViewController: UIViewController {
    @IBAction func didTouchUpInsideButton(sender: Any) {
        let viewController = UIViewController()
        
        // navigationControllerが存在すればpushViewController()を実行
        navigationController?.pushViewController(viewController, animated: true)
    }
}

オプショナルチェーンを用いればOptionalなClosureをunwrapせずに実行させることも可能です。
この場合の()はClosureの実行を意味しているので、これもオプショナルチェーンの応用例といえます。

オプショナルチェーンの使用例2
func perform(completionHandler: (() -> Void)?) {
    completionHandler?() // completionHanlderがnilでなければ実行
}

⑥ Switch meets Optional

unwrapせずにOptional型をそのままSwitchで簡潔に書くことができます。
非常にシンプルで、パターンの後ろに?をつけるだけです。
ただし、Optional型はenumであり、.none(つまりnil)と.some(Wrapped)の2つのcaseを持つためswitch文のcaseとしてnilを考慮する必要があります。

Optionalの定義
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

下記のようにOptionalであっても、defaultを使わずに全caseを?を使って列挙させるような書き方ができるため、enumのcaseがあとから変更されてしまっても、コンパイルエラーですぐに気づくことができるというメリットがあります。
無駄にswitchの前でguard letなどを書かなくても良いので1箇所にロジックが集約できるという利点もありますね。

Optional型をswitchさせる例
class TableViewController: UITableViewController {
    enum Section: Int {
        case header, main, footer
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let section = Section(rawValue: section) // Section?型
        
        switch section {
        case .header?: // .some(let section) where section == .headerと同じ
            return 1
        case .main?:
            return 5
        case .footer?:
            return 1
        case nil: // .none: と同じ
            fatalError()
        }
    }
}

⑦ flatMapで別の型を生成する

よくこういうシチュエーションがあると思います。
正攻法で行くと、わざわざString?型の変数をif letを用いてunwrapしてURL型を生成しないといけないのですが、たったこれだけの処理をするにしては、複数行書かないといけなくてめんどくさいです。

StringのOptional型からURLを生成するロジック例
let urlString: String? = "http://hoge.com" // StringのOptionalの変数

let url: URL?
if let urlString = urlString {
    url = URL(string: urlString)
} else {
    url = nil
}

こういう時は、flatMapを用いるととってもシンプルに書けます:thumbsup:

flatMapの引数にメソッドを渡す例
let urlString: String? = "http://hoge.com" // StringのOptionalの変数
let url = urlString.flatMap(URL.init)

あまり見かけない記法だと思う方もいると思いますが、一体どういう仕組みなのかを説明していきます。

flatMapの定義を見てみます。
flatMapはOptional以外にSequenceなどにも定義されてますが、今回はOptionalのflatMapを見ていきます。

Optional.flatMapの定義
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
}

WrappedはOptionalのGeneric Typeです。
flatMap()は、そのWrappedの引数からGeneric TypeのU?型を返却するClosureであるtransformを引数に持ちます。
Optionalが.someのとき(nilでない)はtransformが実行されてその返り値Uを返します。
もし.none(nil)だった場合は、そのままnilを返すため、最終的にはU?型が返却される関数となっています。

先ほどの例において、変数urlStringはStringのOptional型であるため、flatMapを使うことができます。
さらに、URLのStringを引数にとるinitializerであるinit(string:)はURL型を返すため、flatMapの引数transformの型に等しく、引数として渡すことが可能ということがわかります。
また、URL型はデフォルトでは、Stringの引数1つのみを必要とするInitializerはinit(string:)がユニークであるため、このケースでは引数部分の記述を省略でき、URL.initのみで定義が可能です。

URL(string:)とかけると非常にわかりやすいですが、Swift4.2現在ではそのように記述するとコンパイルエラーとなってしまうため、できないようです。
おそらくこの書き方だとメソッドとして解釈がされないのだと思われます。

もしそのように書きたい場合は、Closureを使ってこのように書くことでも実現可能です。

flatMapと匿名関数を用いた例
let urlString: String? = "http://hoge.com" // StringのOptionalの変数
let url = urlString.flatMap { URL(string: $0) }

Closure編

⑧ Trailing Closure

Swiftはメソッドの末尾の引数がClosureの場合は、引数名を省略して処理を書くことができます。
これをTrailing Closureといいます。
例を見てみるとわかりやすいかと思います。

TrailingClosureの例
// 引数名を省略しない書き方
UIView.animate(withDuration: 0.5, animations: { /* hoge */ })

// Trailing Closure
UIView.animate(withDuration: 0.5) { /* hoge */ }

どうでしょうか。
引数名を省略できた分だけ、短く書けているのがわかるかと思います。

ただし、ここからが重要でTrailing Closureは便利な反面、使い方に注意が必要です。
例えば、こんな状況ではどうでしょうか。

TrailingClosureのアンチパターン例
UIView.animate(withDuration: 0.5, animations: { /* hoge */ }) { (_) in
    // fuga
}

Closureの引数を複数渡すメソッドにTrailing Closureを用いると、1つ目のClosure引数名はわかっても、2つ目の引数名がわからないため、いつ実行されるかが呼び出し側では一目でわからなくなってしまい、意図しない不具合を生んでしまう危険性が高くなってしまいます。
複数のClosure引数を持つメソッドの場合はTrailing Closureを使わない方がベターでしょう。
また、SwiftのLinterとしてメジャーなRealm製のSwiftLintもデフォルト機能として警告を出すように設定されています。

Closure引数を複数持つメソッドの呼び出し例
UIView.animate(withDuration: 0.5, animations: { /* hoge */ }, completion: { (_) in /* fuga */ })

複数のClosure引数を持つメソッドは、上記のようにどちらの引数名も明示して書くようにしましょう。
これによって、animation時の処理とcompletion(完了)時の処理の2つがClosureで書かれているということがわかるため混乱の原因にならなくて済みます。

⑨ 変数の初期化と下処理をClosureでまとめる

さきほどのOptionalなClosureの実行でも述べましたが、Closureとその実行を表す演算子である()を用いることで下処理を含んだ変数定義をネストを掘ってまとめることができます。
lazy varを書くときにも、よくこの書き方がされると思います。

Closureを用いた変数定義例
func setup() {
    // △: 全部同一スコープでパッと見の処理が追いにくい
    let label = UILabel()
    label.backgroundColor = .red
    label.font = .systemFont(ofSize: 10.0)
    view.addSubview(label)
    let button = UIButton()
    button.setTitle("Button", for: .normal)
    button.addTarget(self, action: #selector(hoge), for: .touchUpInside)
    view.addSubview(button)
    
    // ○: 変数の定義をClosureとその実行()を使って責務を分けると処理が追いやすい
    let label: UILabel = {
        let label = UILabel()
        label.backgroundColor = .red
        label.font = .systemFont(ofSize: 10.0)
        return label
    }()
    view.addSubview(label)
    let button: UIButton = {
        let button = UIButton()
        button.setTitle("Button", for: .normal)
        button.addTarget(self, action: #selector(hoge), for: .touchUpInside)
        return button
    }()
    view.addSubview(button)
}

また、下記の場合のようにClosure内のスコープでは早期returnも可能になるため、簡潔に書けるのも強みです。
特に恩恵を受けるのはinitializerの中での処理でしょうか。
初期化されるまでは定義したインスタンスメソッドは呼び出すことはできないため、initの内部にclosureを使ってロジックを埋め込む必要が出てきますが、そういったときにも力を発揮してくれると思います。

変数定義にclosureと実行を使って早期returnする例
struct Model {
    enum Status {
        case registered(id: Int, password: String)
        case unregistered
    }
    
    let status: Status
    let name: String?
    
    init(dictionary: [String: Any]) {
        // 変数ごとにパースロジックを定義すると読みやすい
        status = {
            guard let id = dictionary["id"] as? Int,
                let password = dictionary["password"] as? String else {
                return .unregistered // early returnが可能
            }
            return .registered(id: id, password: password)
        }()
        name = dictionary["name"] as? String
    }
}

番外編: 命名規則

記法ではないですが、普段気をつけている命名規則についてまとめます。
基本的には、Swift.org - API Design Guidelinesに従って書くようにするのが良いと思います。

⑩ Delegateを作るときは標準ライブラリの命名規則に従う

みなさんは、カスタムのDelegateをつくるときの命名に気を配ってますでしょうか。

ちょっとアンチパターンの例を示したいと思います。

Delegateのアンチパターン
protocol FirstTableViewCellDelegate {
    func didSelectAction()
}

FirstTableViewCellというクラスのDelegateということはprotocol名を見るとわかります。
また、メソッド名からCellの中にActionを実行できる何かがいて、それが選択された際の処理を委譲してることがわかります。

おそらくTableViewを管理するVCクラスなどがこのDelegateを実装すると思いますが、もし同じようなCellが存在して同じようにDelegateを実装しないといけなくなったとしたらどうなるでしょうか。
FirstTableViewCellと同じようにAction機能を実装したSecondTableViewCellがいた時に同様のメソッド名を持つDelegateをVCが実装しようとすると、同名のメソッドを定義できずコンパイルエラーになってしまいます。

Delegateの実装
class ViewController: UIViewController {}
extension ViewController: FirstTableViewCellDelegate {
    func didSelectAction() {
        print("FirstTableViewCellのActionが選択されたよ")
    }
}
extension ViewController: SecondTableViewCellDelegate {
    // 同名のメソッドは定義できないのでCompile Errorとなる
    func didSelectAction() {
        print("SecondTableViewCellのActionが選択されたよ")
    }
}

では、CocoaやUIKitといった標準ライブラリはどのように命名をしているかを見ていきます。
非常によく使うUITableViewDataSourceならイメージがしやすいと思います。

UITableViewDataSourceのDelegateメソッド
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func numberOfSections(in tableView: UITableView) -> Int

これを見たらすぐにわかると思いますが、ポイントは下記の通りです。

  • Delegate methodであると一目でわかるように、委譲したインスタンス自身を必ず第一引数に指定している
  • 自身しか引数がない場合は、メソッド名に委譲する処理の詳細(numberOfSections)を指定する
  • 2つ以上引数を持つ場合は、メソッド名にインスタンスのクラス名を指定(tableView)し、第一引数の外部引数名は冗長になるので省略(_)、第二引数の外部引数名に委譲する処理の詳細(numberOfRowsInSection)を指定する

一番最後のポイントはかなり英語のセンスを問われるかもしれませんが(笑)、メソッド・外部引数・内部引数名が一つの英文みたいにつながるように書かれています。
これを実践できるかが、より標準ライブラリぽく、そしておしゃれなDelegateを作る鍵といえます:key:

先ほどの例を標準ライブラリの命名に準拠して書き直してみます。
どうでしょうか。
名前空間の問題も起こらず、どちらのDelegateも問題なく実装することができました:tada:

標準ライブラリに準拠したカスタムDelegateの実装例
protocol FirstTableViewCellDelegate {
    func didSelectAction(on firstTableViewCell: FirstTableViewCell)
}
protocol SecondTableViewCellDelegate {
    func didSelectAction(on secondTableViewCell: SecondTableViewCell)
}

class ViewController: UIViewController {}
extension ViewController: FirstTableViewCellDelegate {
    // 引数に委譲されてきたインスタンスがあるのでDelegateメソッドだとわかる
    func didSelectAction(on firstTableViewCell: FirstTableViewCell) {
        print("FirstTableViewCellのActionが選択されたよ")
    }
}
extension ViewController: SecondTableViewCellDelegate {
    // Firstとはメソッド定義が異なるためCompile Errorにならない
    func didSelectAction(on secondTableViewCell: SecondTableViewCell) {
        print("SecondTableViewCellのActionが選択されたよ")
    }
}

おわりに

いかがでしたでしょうか。
一つでも参考になったり発見があったら嬉しいです!

余談ですが、型省略に頼って.(ピリオド)を型なしで入力して補完をきかせようとするとXcodeが全然サジェストしてくれないことが多々あります:cry:
いつか型省略による補完もばっちり効くようになってほしいです。

最後までお読みいただきありがとうございました:bow:
ここに書かれた以外の記法を推しの方がいましたら、コメントなどで教えていただけると幸いです!

242
187
7

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
242
187

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?