特定の端末だけで起こる端末依存のバグ対処に直感的に場合分けして対応できるiOSデバイスチェッカーを作ってみた

## はじめに
ある特定の iOS のバージョンで起こるバグは，iOS のバージョンを UIDevice などで取得して場合分けすることでなんとかなる。ある特定の端末サイズでのみで起こるバグは，端末サイズを UIScreen などで取得することでなんとかなる。
しかし，iPad Air 2 だけとか，iPad mini 1 だけとかで起こるバグに関してはこれらは通用しない。特定の端末のみのバグ対処をしなければならない場面があったので，サンプルを書いてみた。ただ作るだけだと面白くないので enum を使うことで，直感的に場合分けできるようにした。Swift 版はできたらまた書きます。

## サンプルコード

GitHub にサンプルコードをあげましたので，
よろしければご覧下さい。

**[OCDeviceManager(GitHib)](https://github.com/MilanistaDev/OCDeviceManager)**

**2017.01.10 追記**
Swift 3 でも書いてみました。ただ，移植しただけなので
Objective-S みたいなコードになっています。

**[DeviceManager(GitHib)](https://github.com/MilanistaDev/DeviceManager)**

もっとこうすればスマートだよとか，
普通はこういう書き方するよねとか
アドバイスいただければ嬉しいです。

![IMG_3050.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/59582263-463c-b0ea-5948-7ace565bfe15.png "IMG_3050.PNG")


## 使い方

1.下記の3ファイルをプロジェクトに追加します。

> DeviceList.plist
> DeviceManager.h
> DeviceManager.m

2.使用するクラスファイルで下記のように使用します。

```ViewController.m
// ヘッダファイルを追加します
# import DeviceManager.h
…

// 端末の種類を取得
DeviceType deviceType = [DeviceManager currentDeviceType];
// enum で直感的に判定できるようにしました
if (deviceType == iPhone6) {
	// iPhone 6 だったら例えば背景色を赤くしてやる
    self.view.backgroundColor = [UIColor redColor];
} else if (deviceType == iPadAir2) {
	// iPad Air 2 だったら例えば背景色を緑にしてやる
	self.view.backgroundColor = [UIColor greenColor];
}
```

端末識別に使う enum は下記のように DeviceManager.h で定義されています。

```DeviceManager.h

// アプリ対応端末 enum
// プロジェクトによって増やしたり減らしたりする
typedef NS_ENUM (NSInteger, DeviceType) {
    Unknown = 0,
    iPhone4S,
    iPhone5,
    iPhone5c,
    iPhone5s,
    iPhone6Plus,
    iPhone6,
    iPhone6s,
    iPhone6sPlus,
    iPhoneSE,
    iPad3,
    iPad4,
    iPadAir,
    iPadAir2,
    iPadmini1,
    iPadmini2,
    iPadmini3,
    iPadmini4,
    iPadPro9_7,
    iPadPro12_9,
    iPodtouch5,
    iPodtouch6,
};
```

アプリにおいて対応させる端末によって
適宜追加修正が必要なのが，よくない点です。
新規端末が発売されたとき，対応させる端末外に
なったとき修正がどうしても必要となってしまいます。

## コードについて
モデル名を取得して，その文字列比較を行っています。
currentDeviceIdentifierName は例えば，
```iPhone5,1``` などの文字列を返します。
この文字列を取得して直接使うことはないとは思いますが，
なにかあるかもしれないのでクラスメソッド化しています。

```DeviceManager.m
// 見慣れぬこれらのヘッダファイルを追加
#include <sys/types.h>
#include <sys/sysctl.h>

// モデル名を返す
+ (NSString *)currentDeviceIdentifierName {
    NSString *platformName = @"";
    size_t size = 0;
    if ((0 == sysctlbyname("hw.machine", NULL, &size, NULL, 0)) && (size > 0)) {
        char *machine = malloc(size);
        if (machine != NULL) {
            sysctlbyname("hw.machine", machine, &size, NULL, 0);
            platformName = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
            free(machine);
        }
    }
    return platformName;
}
```

文字列比較で DeviceType を返すように実装しています。
Objective-C では，switch 文で文字列比較ができないので，
マクロを利用しています。この記事[^1]を参照させていただきました。

```DeviceManager.m
// 認識しやすい名前で返す
+ (DeviceType)currentDeviceType {
    NSString *machineName = [[self class] currentDeviceIdentifierName];
    DeviceType deviceType = Unknown;

    SWITCH (machineName) {
        CASE (@"iPhone4,1") {
            deviceType = iPhone4S;
            break;
        }
    ...
    // 省略
    // マクロの書き方次第ではもっとスマートに書けそう
    // DeviceList.plist もうまく使えばメンテコスト減らせるかも 
    ...
}
```

DeviceType を返すように作ったので，
下記のように開発者にわかりやすい，
直感的な書き方で場合分けが可能になります。

```ViewController.m
// 端末の種類を取得
DeviceType deviceType = [DeviceManager currentDeviceType];
// enum で直感的に判定できるようにしました
if (deviceType == iPhone6) {
	// iPhone 6 でのみ起こるバグ修正
} else if (deviceType == iPadAir2) {
	// iPad Air 2 でのみ怒るバグ修正
	self.view.backgroundColor = [UIColor greenColor];
}
```

DeviceList.plist は何のために作ったの？
plist はモデル名と NSInteger が対になった，
Dictionary になっており，一応こちらを使うことで
端末名自体は取得可能になっています。

```ViewController.m

@property (nonatomic, copy) NSDictionary *deviceDic;

// DeviceList.plist からデータ取得
NSBundle *bundle = [NSBundle mainBundle];
// DeviceList.plist のファイルパスを指定
NSString *path = [bundle pathForResource:@"DeviceList" ofType:@"plist"];
// DeviceList.plistの中身データを取得して格納
self.deviceDic = [NSDictionary dictionaryWithContentsOfFile:path];


// iPhone5,1 みたいな文字列取得
NSString *modelName = [DeviceManager currentDeviceIdentifierName];
self.modelNumberLabel.text = modelName;

DeviceType deviceType = [DeviceManager currentDeviceType];

NSString *deviceTypeStr = [NSString stringWithFormat:@"%ld", (long)deviceType];
NSString *deviceName = [self.deviceDic objectForKey:deviceTypeStr];
```

書いてて思った。
Dictionary 何だから，キーはモデル名，値は端末名の方がいいですね。
DeviceType ではなくモデル名を取得して，値(端末名)取得の方が・・・
主たる目的は端末名で場合分けして処理をすることなのでまぁいっか。

## 問題点
* 新機種，非対応になった端末が出た際のメンテナンス
* モデル名と比較する際の SWITCH 文の多さ(ダサさ)

## おわりに
特定の端末だけおこるバグ。できれば受入試験などで出てきて欲しくないものですが，やっぱりどうしても出てきてします。一度こういう iOS デバイスチェッカー
みたいなのを作っておくと便利かもしれません。とはいえ，メンテの必要性，新たなデグレの誘発などを起こす可能性も出てきます。できればこういう対処でなく，Apple になんとかしてくれと頼んでみるのが先なのかもしれません。あくまでも応急処置がのぞましい。
私の現場ではこういう書き方してるよーとか，
こうすればもっと可読性良くなるよーとか，
もっとスマートな判定の仕方等あればご指摘いただければ嬉しいです。

### 参考
[^1]: [Switchの文字列比較を自作 Objective-C](http://qiita.com/kazTera/items/f4b8f92cda6551330cc2)

[OCDeviceManager(GitHub：この記事のサンプルコード)](https://github.com/MilanistaDev/OCDeviceManager)