LoginSignup
22

More than 3 years have passed since last update.

[Swift]ProtocolとEnumでSwiftライクな一元管理で綺麗なコードを書く工夫

Last updated at Posted at 2018-02-27

前置き

Swiftをちゃんと業務で書き始めて、やっと1年くらいたったのですが、、、
いつの間にか自分の中でよく使う書き方になっていたので、記録的な意味も込めて書いておきます。

こういう記事もありますし、
- Swift では Protocol を積極的に使おう

もっとこうしたらとかあればご指摘お願いいたします。

一元管理

【UIView / UITableviewCell】

「脱StoryBoardはしているが、CustomViewは使っている」なんて人は多いんじゃないかと思います。
そんな人にオススメです。

① ベースとなる共通プロトコル作成

protocol BaseViewType {
    var name: String { get } // クラス名 and セル再利用ID
    var nib: UINib { get } // UINib参照
    var view: UIView? { get } // View呼び出し
}

② カスタムビューのタイプごとにプロトコルの実装

例としてわかりやすい適当なクラス名をつけています。

/* UIView のタイプ */
enum UIViewType: BaseViewType {
    case header
    case footer

    var name: String {
        switch self {
        case .header: return "HeaderView"
        case .footer: return "FooterView"
        }
    }

    var nib: UINib {
        return UINib(nibName: name, bundle: nil)
    }

    var view: UIView? {
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

/* UITableViewCell のタイプ */
enum UITableViewCellType: BaseViewType {
    case article
    case movie
    case sns(type: SnsType) // ・・・・・・・・・注①

    var name: String {
        switch self {
        case .article: return "ArticleTableViewCell"
        case .movie: return "MovieTableViewCell"
        case .sns(let type): return type.name
        }
    }

    var nib: UINib {
        return UINib(nibName: name, bundle: nil)
    }

    var view: UIView? {
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }

}

enum SnsType { // ・・・・・・・・・注①
    case facebook
    case instagram

    var name: String {
        switch self {
        case .facebook: return "FacebookTableViewCell"
        case .instagram: return "InstagramTableViewCell"
        }
    }
}

/* 以下、必要なタイプごとに増えていく */
enum UICollectionViewCellType: BaseViewType {
    // 略
}

// ・
// ・
// ・
// ・

どのレイヤーでビュータイプを定義分割するかは個人の判断によるかと思いますが、
自分が行なっているプロジェクトでは、例えばテーブルだけでも
- テーブルセル
- テーブルヘッダー
- テーブルフッター
- テーブルセクション
と細かく分けています。

タイプごとでさらに階層構造になる場合は、注①のようにすることで対応可能です。
(わかりやすくあえてenumを外だししていますが、enum内のenumで定義した方がより堅固なコードになります)

使用例

例1 UITableView Header / Footer のセット

TableViewHeaderFooterをセットするときなど、以下のように1行でスッキリ書くことができる。

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    return UIViewType.header.view as? HeaderView
}

func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
    return UIViewType.footer.view as? FooterView
}

Viewの中身を動的に何か操作する場合は、少し操作が必要になるが、それでも2、3行で済む、、、はず笑

例2 UITableViewCell 呼び出し

以下、呼び出すのに必要最低限のみコードのみ記載しています。


/* ①テーブルにセルの登録 */
@IBOutlet fileprivate weak var tableView: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.register(UITableViewCellType.article.nib, forCellReuseIdentifier: UITableViewCellType.article.name)
}   

/* ②セルの呼び出し */
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(
        withIdentifier: UITableViewCellType.article.name,
        for: indexPath
    ) as! ArticleTableViewCell

   // do some setup

    return cell
}

ポイント

  • 事前に定義しておくUINibを呼び出す際のクラス名のタイポを防げる
  • 新たにカスタムビューを作成しても、タイプを増やすだけですぐに適応し呼び出せる

【Firebase Analytics】

Firebaseに限らずログを複数送る系はだいたい当てはまるかと思います。

① ベースとなる共通プロトコル作成

protocol BaseEventModel {
    var eventName: String { get } // ログの送信するイベント名
    var eventParameters: [String: Any] { get } // ログの送信するパラメーター
    func logEvent() // ログ送信の実行メソッド
}

eventParameters[String: Any](バリューをAny型でセットできるよう)にして、どんなパラメーターが来ても対応できるようになっている。

② ログごとにプロトコルの実装

例としてわかりやすい適当なパラメータやキー名をつけています。

1つ目のイベントは「記事の操作イベント」
2つ目のイベントは「タップの検知イベント」
上記のようなログを取りたい想定として、以下を見ていただけると。

/* 1つ目のイベントタイプ */
enum ArticleEvents: BaseEventModel {
    case searchArticle(searchTerm: String)
    case shareArticle(articleId: Int, articleTitle: String)

    var eventName: String {
        switch self {
        case .searchArticle: return "search_article"
        case .shareArticle: return "share_article"
        }
    }

    var eventParameters: [String: Any] {
        var params: [String: Any] = [:] // [キー: バリュー]

        switch self {
        case .searchArticle(let term):
            params[term] = term

        case .shareArticle(let id, let title):
            params[article_id] = id
            params[article_title] = title

        }

        return params
    }

    func logEvent() {
        Analytics.logEvent(self.eventName, parameters: self.eventParameters)
    }

}

/* 2つ目のイベントタイプ */
enum TapEvents: BaseEventModel {
    case tapNavigation
    case tapNotification(isTapped: Bool)

    var eventName: String {
        switch self {
        case .tapNavigation: return "tap_navigation"
        case .tapNotification: return "tap_notification"
        }
    }

    var eventParameters: [String: Any] {
        var params: [String: Any] = [:]

        switch self {
        case .tapNavigation: 
            break         

        case .tapNotification(let isTapped):
            params[is_tapped] = isTapped         

        }

        return params
    }

    func logEvent() {
        Analytics.logEvent(self.eventName, parameters: self.eventParameters)
    }

}

/* 以下、必要なイベントごとに増えていく */
enum MoveEvents: BaseEventModel {
    // 略
}

// ・
// ・
// ・
// ・

使用例

ログを送りたい箇所に、以下のようにする。

ArticleEvents.searchArticle(searchTerm: "検索ワード").logEvent()
ArticleEvents.shareArticle(articleId: 12345, articleTitle: "タイトル").logEvent()

TapEvents.tapNavigation.logEvent()
TapEvents.tapNotification(isTapped: true).logEvent()

ポイント

  • パラメーターのセットからログの送信までワンライナー簡潔に書ける

備考

GoogleAnalyticsで、過去に同じようにやられてる方を見つけたので参考までに
- GoogleAnalyticsをenumで

【UserDefaults】

今回は以下4つを保存する例を記載します。
- email (String型)
- id (Int型)
- isRegistered (Bool型)
- date (Date型)

呼び出し関数が多かったので、複数定義しておらずプロトコルを作成していませんが、プロジェクトなどでお好みで個々に作成していただければと思います。

enum UserDefaultsType {
    case email
    case id
    case isRegistered
    case date

    // UserDefaultsの呼び出し
    private var ud: UserDefaults {
        return UserDefaults.standard
    }

    // Keyの設定(キーはなんでも良い。一度設定してしまえばタイプミスも防げる)
    private var key: String {
        switch self {
        case .email: return "user_email"
        case .id: return "user_id"
        case .isRegistered: return "user_isRegistered"
        case .date: return "user_date"
        }
    }

    // Set Method

    // ジェネリクス関数で型に左右されず一括でセットでできる
    func setItem<T>(with item: T) {
        ud.set(item, forKey: key)
    }


    // Get Methods

    // 型ごとにゲットメソッドを定義。必要な型に応じてここは増やしてください。
    func getString() -> String? {
        return ud.string(forKey: key)
    }

    func getInt() -> Int {
        return ud.integer(forKey: key)
    }

    func getBool() -> Bool {
        return ud.bool(forKey: key)
    }

    func getDate() -> Date? {
        return ud.object(forKey: key) as? Date
    }


    // Remove Method

    // 削除メソッド
    func removeItem() {
        ud.removeObject(forKey: key)
    }
}

使用例


// set
UserDefaultsType.email.setItem(with: "xxx@xx.jp")
UserDefaultsType.id.setItem(with: 42543)
UserDefaultsType.isRegistered.setItem(with: false)
UserDefaultsType.date.setItem(with: Date())

// get
UserDefaultsType.email.getString() // 注: Optional
UserDefaultsType.id.getInt()
UserDefaultsType.isRegistered.getBool()
UserDefaultsType.date.getDate() // 注: Optional

// remove
UserDefaultsType.email.removeItem()
UserDefaultsType.id.removeItem()
UserDefaultsType.isRegistered.removeItem()
UserDefaultsType.date.removeItem()

ポイント

  • キー設定しておけるので、使用するさいのタイプミスを防げる。
  • setItemジェネリクス関数なので型を気にする必要がない

備考

古い記事として
- NSUserDefaultsを便利に使う方法を幾つか
- enumでNSUserDefaultsを便利に

などありますが、個人的に呼び出し元で
- メソッドごとにUserDefaults.standardを書いてる
- 呼び出し元でキャストしてる
しているのがスマートじゃないと思ったので、上記のようにしています。

あとがき

enumでただ列挙するのではなく、 Protocolである程度縛りのある一元管理ができたのではないかと思います。
一元管理するための定義コードは若干長くなりますが、最初にちゃんと定義しておくことで、あとあと呼び出し元で綺麗で簡潔に呼び出せるコードになっているかと!

もっとこういうのあるぜ、っていうの教えていただけると嬉しいです。

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
22