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


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

こういう記事もありますし、
- [Swift では Protocol を積極的に使おう](https://qiita.com/mokumoku/items/2744c68fdb4bb53d0376)

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

# 一元管理

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

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

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

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

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

```swift
/* 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で定義した方がより堅固なコードになります)


### 使用例

#### 例１ UITableView Header / Footer のセット
``TableView``の``Header``や``Footer``をセットするときなど、以下のように１行でスッキリ書くことができる。

```swift
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の中身を動的に何か操作する場合は、少し操作が必要になるが、それでも２、３行で済む、、、はず笑


#### 例２ UITableViewCell 呼び出し

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

```swift

/* ①テーブルにセルの登録 */
@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``に限らずログを複数送る系はだいたい当てはまるかと思います。


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

```swift
protocol BaseEventModel {
    var eventName: String { get } // ログの送信するイベント名
    var eventParameters: [String: Any] { get } // ログの送信するパラメーター
    func logEvent() // ログ送信の実行メソッド
}
```
``eventParameters``を``[String: Any]``（バリューを**Any**型でセットできるよう）にして、どんなパラメーターが来ても対応できるようになっている。

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

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

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

```swift
/* １つ目のイベントタイプ */
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)
    }

}

/* ２つ目のイベントタイプ */
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 {
    // 略
}

// ・
// ・
// ・
// ・
```

### 使用例

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

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

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

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

### 備考
GoogleAnalyticsで、過去に同じようにやられてる方を見つけたので参考までに 
- [GoogleAnalyticsをenumで](https://qiita.com/morizotter/items/feafad2fd4dce6398a3d)



## 【UserDefaults】

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

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

```swift
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)
    }
}
```

### 使用例

```swift

// 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を便利に使う方法を幾つか](https://qiita.com/taji-taji/items/edc1bad4709ce0e0a154)
- [enumでNSUserDefaultsを便利に](https://qiita.com/Akkyie/items/9a5bd66951615073c080)

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


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

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