クリスマス前にARでジョークアプリをつくってみた話

## 序
この記事は[iOS #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ios-2)の19日目の記事になります。
あんまり今年は書くモチベなかったのですが、ちょうどわいたところに空きがあって飛び入り参加しました。

とうようです。

普段は個人でiOSアプリつくったり、大学院でUIの研究室にいたり、[中高生にITを教えたり](https://life-is-tech.com/leaders/)しています。
ちなみに大学生・大学院生向けにちょうどこの**Life is Tech !**という中高生にITを教える団体のメンター募集がはじまっているので良かったら応募してみてください！笑
楽しい上にいろんなスキルが身につくのでぜひ！

＞ーー切り取り線ーー＜

さて、本題。

## はじまり

はじまりはこの秋、2019年9月にさかのぼります。

増税前のiPhone 11シリーズの発表。僕は長年iPhone 7 Plusを利用していたのですが、そろそろ欲しいと。
Face ID使ってみたいと。

そんなわけで**SIMフリーのiPhone 11 Pro Max**を購入しました！（パチパチパチ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/00e7d59a-65c9-7373-1d83-003469ca5cb6.png)


今までの機体も開発用に残しておこうという判断をした結果、手元にはこのようにiPhone 7 PlusとiPhone 11 Pro Maxの二台が残りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/92db7991-8a9e-0ff7-bcab-e508b9d1096b.png)

そう二台が...。

**あっ！！！！！！！！！！**

そこで気付いてしまったのです。この二台がある意味を...。
思いついてしまったものは仕方がないと、早速制作をはじめました。

## 手始め

まず手始めにARに表示するオブジェクトを制作しようということになりました。

といっても~~こんなにくだらない~~アプリにわざわざMayaやBlendarを使うのもなぁということで最初に検討したのがこれ

**Reality Composer!!!**

これはiOS13とXcode11向けにWWDC2019で発表されたApple公式の3Dモデル製作ツールです。そう、Apple公式の。大事なことなので２回言いました。

触ってみるとすごく簡単にリッチな3Dモデルがつくれてめっっっっちゃ楽しいです。詳しいことは[こちら](https://note.com/northsand/n/nb245a2d4ab1f)の記事などをご覧ください。

ですがこれは自分のやりたかったことに対して自由度が少し低い（ような気がする）という欠点がありました。


しかも今回つくらなければいけないのはめちゃくちゃ単純なモデルです。
そこで次に採用を決めたのがXcodeについているSceneKitのモデル製作画面です。

[こちらの記事](https://qiita.com/k-boy/items/14205dd569a57eb44d96)が参考になります。

というわけでとても簡単な平面のモデルをつくりました。えいやっ！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/df69004d-9b5a-d16c-6853-4005479bd4a2.png)

実際の模様はコードで反映していくので現時点ではめちゃくちゃ簡単です。

## アプリの機能を変える

続いてこのアプリはiPhone 7 PlusとiPhone 11 Pro Maxの役割を完全に分けようという方針を考えたので、その実装をはじめました。

そのためにはデバイスの種類が取れるようにしなければいけません。なので[こちら](https://qiita.com/MYamate_jp/items/9f26ad6f78f347ebd629)を参考にこのような関数を追加しました。

```swift
func getDeviceInfo() -> String {
    var size: Int = 0
    sysctlbyname("hw.machine", nil, &size, nil, 0)
    var machine = [CChar](repeating: 0, count: Int(size))
    sysctlbyname("hw.machine", &machine, &size, nil, 0)
    let code: String = String(cString: machine)

    let deviceCodeDic: [String: String] = [
        /* Simulator */
        "i386"      : "Simulator",
        "x86_64"    : "Simulator",
        /* iPod */
        "iPod1,1"   : "iPod Touch 1st",            // iPod Touch 1st Generation
        "iPod2,1"   : "iPod Touch 2nd",            // iPod Touch 2nd Generation
        "iPod3,1"   : "iPod Touch 3rd",            // iPod Touch 3rd Generation
        "iPod4,1"   : "iPod Touch 4th",            // iPod Touch 4th Generation
        "iPod5,1"   : "iPod Touch 5th",            // iPod Touch 5th Generation
        "iPod7,1"   : "iPod Touch 6th",            // iPod Touch 6th Generation
        /* iPhone */
        "iPhone1,1"   : "iPhone 2G",                 // iPhone 2G
        "iPhone1,2"   : "iPhone 3G",                 // iPhone 3G
        "iPhone2,1"   : "iPhone 3GS",                // iPhone 3GS
        "iPhone3,1"   : "iPhone 4",                  // iPhone 4 GSM
        "iPhone3,2"   : "iPhone 4",                  // iPhone 4 GSM 2012
        "iPhone3,3"   : "iPhone 4",                  // iPhone 4 CDMA For Verizon,Sprint
        "iPhone4,1"   : "iPhone 4S",                 // iPhone 4S
        "iPhone5,1"   : "iPhone 5",                  // iPhone 5 GSM
        "iPhone5,2"   : "iPhone 5",                  // iPhone 5 Global
        "iPhone5,3"   : "iPhone 5c",                 // iPhone 5c GSM
        "iPhone5,4"   : "iPhone 5c",                 // iPhone 5c Global
        "iPhone6,1"   : "iPhone 5s",                 // iPhone 5s GSM
        "iPhone6,2"   : "iPhone 5s",                 // iPhone 5s Global
        "iPhone7,1"   : "iPhone 6 Plus",             // iPhone 6 Plus
        "iPhone7,2"   : "iPhone 6",                  // iPhone 6
        "iPhone8,1"   : "iPhone 6S",                 // iPhone 6S
        "iPhone8,2"   : "iPhone 6S Plus",            // iPhone 6S Plus
        "iPhone8,4"   : "iPhone SE" ,                // iPhone SE
        "iPhone9,1"   : "iPhone 7",                  // iPhone 7 A1660,A1779,A1780
        "iPhone9,3"   : "iPhone 7",                  // iPhone 7 A1778
        "iPhone9,2"   : "iPhone 7 Plus",             // iPhone 7 Plus A1661,A1785,A1786
        "iPhone9,4"   : "iPhone 7 Plus",             // iPhone 7 Plus A1784
        "iPhone10,1"  : "iPhone 8",                  // iPhone 8 A1863,A1906,A1907
        "iPhone10,4"  : "iPhone 8",                  // iPhone 8 A1905
        "iPhone10,2"  : "iPhone 8 Plus",             // iPhone 8 Plus A1864,A1898,A1899
        "iPhone10,5"  : "iPhone 8 Plus",             // iPhone 8 Plus A1897
        "iPhone10,3"  : "iPhone X",                  // iPhone X A1865,A1902
        "iPhone10,6"  : "iPhone X",                  // iPhone X A1901
        "iPhone11,8"  : "iPhone XR",                 // iPhone XR A1984,A2105,A2106,A2108
        "iPhone11,2"  : "iPhone XS",                 // iPhone XS A2097,A2098
        "iPhone11,4"  : "iPhone XS Max",             // iPhone XS Max A1921,A2103
        "iPhone11,6"  : "iPhone XS Max",             // iPhone XS Max A2104
        "iPhone12,5"  : "iPhone 11 Pro Max",         // iPhone 11 Pro Max

        /* iPad */
        "iPad1,1"   : "iPad 1 ",                     // iPad 1
        "iPad2,1"   : "iPad 2 WiFi",                 // iPad 2
        "iPad2,2"   : "iPad 2 Cell",                 // iPad 2 GSM
        "iPad2,3"   : "iPad 2 Cell",                 // iPad 2 CDMA (Cellular)
        "iPad2,4"   : "iPad 2 WiFi",                 // iPad 2 Mid2012
        "iPad2,5"   : "iPad Mini WiFi",              // iPad Mini WiFi
        "iPad2,6"   : "iPad Mini Cell",              // iPad Mini GSM (Cellular)
        "iPad2,7"   : "iPad Mini Cell",              // iPad Mini Global (Cellular)
        "iPad3,1"   : "iPad 3 WiFi",                 // iPad 3 WiFi
        "iPad3,2"   : "iPad 3 Cell",                 // iPad 3 CDMA (Cellular)
        "iPad3,3"   : "iPad 3 Cell",                 // iPad 3 GSM (Cellular)
        "iPad3,4"   : "iPad 4 WiFi",                 // iPad 4 WiFi
        "iPad3,5"   : "iPad 4 Cell",                 // iPad 4 GSM (Cellular)
        "iPad3,6"   : "iPad 4 Cell",                 // iPad 4 Global (Cellular)
        "iPad4,1"   : "iPad Air WiFi",               // iPad Air WiFi
        "iPad4,2"   : "iPad Air Cell",               // iPad Air Cellular
        "iPad4,3"   : "iPad Air China",              // iPad Air ChinaModel
        "iPad4,4"   : "iPad Mini 2 WiFi",            // iPad mini 2 WiFi
        "iPad4,5"   : "iPad Mini 2 Cell",            // iPad mini 2 Cellular
        "iPad4,6"   : "iPad Mini 2 China",           // iPad mini 2 ChinaModel
        "iPad4,7"   : "iPad Mini 3 WiFi",            // iPad mini 3 WiFi
        "iPad4,8"   : "iPad Mini 3 Cell",            // iPad mini 3 Cellular
        "iPad4,9"   : "iPad Mini 3 China",           // iPad mini 3 ChinaModel
        "iPad5,1"   : "iPad Mini 4 WiFi",            // iPad Mini 4 WiFi
        "iPad5,2"   : "iPad Mini 4 Cell",            // iPad Mini 4 Cellular
        "iPad5,3"   : "iPad Air 2 WiFi",             // iPad Air 2 WiFi
        "iPad5,4"   : "iPad Air 2 Cell",             // iPad Air 2 Cellular
        "iPad6,3"   : "iPad Pro 9.7inch WiFi",       // iPad Pro 9.7inch WiFi
        "iPad6,4"   : "iPad Pro 9.7inch Cell",       // iPad Pro 9.7inch Cellular
        "iPad6,7"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad6,8"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad6,11"  : "iPad 5th",                    // iPad 5th Generation WiFi
        "iPad6,12"  : "iPad 5th",                    // iPad 5th Generation Cellular
        "iPad7,1"   : "iPad Pro 12.9inch 2nd",       // iPad Pro 12.9inch 2nd Generation WiFi
        "iPad7,2"   : "iPad Pro 12.9inch 2nd",       // iPad Pro 12.9inch 2nd Generation Cellular
        "iPad7,3"   : "iPad Pro 10.5inch",           // iPad Pro 10.5inch A1701 WiFi
        "iPad7,4"   : "iPad Pro 10.5inch",           // iPad Pro 10.5inch A1709 Cellular
        "iPad7,5"   : "iPad 6th",                    // iPad 6th Generation WiFi
        "iPad7,6"   : "iPad 6th",                    // iPad 6th Generation Cellular
        "iPad8,1"   : "iPad Pro 11inch WiFi",        // iPad Pro 11inch WiFi
        "iPad8,2"   : "iPad Pro 11inch WiFi",        // iPad Pro 11inch WiFi
        "iPad8,3"   : "iPad Pro 11inch Cell",        // iPad Pro 11inch Cellular
        "iPad8,4"   : "iPad Pro 11inch Cell",        // iPad Pro 11inch Cellular
        "iPad8,5"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad8,6"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad8,7"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad8,8"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad11,1"  : "iPad Mini 5th WiFi",          // iPad mini 5th WiFi
        "iPad11,2"  : "iPad Mini 5th Cell",          // iPad mini 5th Cellular
        "iPad11,3"  : "iPad Air 3rd WiFi",           // iPad Air 3rd generation WiFi
        "iPad11,4"  : "iPad Air 3rd Cell"            // iPad Air 3rd generation Cellular
    ]

    if let deviceName = deviceCodeDic[code] {
        return deviceName
    } else {
        if code.range(of: "iPod") != nil {
            return "iPod Touch"
        }else if code.range(of: "iPad") != nil {
            return "iPad"
        }else if code.range(of: "iPhone") != nil {
            print(code)
            return "iPhone"
        }else{
            return "unknownDevice"
        }
    }
}
```

ちなみにdeviceCodeがなんなのかは参考記事以外全然情報を見つけられなかったので自分の機体のcodeを一旦printして付け足しました（本当は全デバイスに対応したい...）

これで準備が整ったので次に特定のデバイスの時だけ処理を切り分けるということを行います。これはデバイスの正式名称が手に入っているので`contains`を使えばいいです。

たとえば今回はiPhone 7系の時だけ別のストーリーボードから出したかったのでAppDelegateにこのように書きました（もちろんSceneDelegateがあるプロジェクトの場合はそちらに書くべき処理です。）

```swift:AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let deviceName = getDeviceInfo()
    let imageStoryboard = UIStoryboard(name: "SevenImage", bundle: nil)
    if deviceName.contains("iPhone 7") {
        window?.rootViewController = imageStoryboard.instantiateInitialViewController()
    }
    return true
}
```

以降の処理の書き分けも同様のif文を用いてできます。

## 画像に反応させる

（だんだんタイトルが真面目になってきましたが）次に考えたのは画像に反応してAR上で何かをするというものです。
代表的なところでいうと[AR名刺](https://qiita.com/k-boy/items/d5c7e0f4174da4b8d5f0)などが有名ですね。あとは海外の中高生デベロッパーがARKit出たてのころ、ゲームのパッケージをアンカーにゲームの紹介映像を流すサンプルアプリを制作してバズったりなんかもしてました。

僕もこれらを参考にやっていきます。
今回アンカーにしたのがこちらの画像

![seven.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/5fa2babc-4875-c710-f481-b4b971588988.png)

最終目標はこの画像をアンカーにしてそこに3DオブジェクトをARで重ねるというものになります。

これはiPhone 7系でアプリを実行すると全画面表示されるようになっています。

つまり！

**iPhone 7系ともう一台のiPhoneがあってはじめて成立するアプリ、それがこのアプリの最終形です！！！！！！！**

気を取り直して、

この具体的なやり方はAR名刺の記事や最後に載せる完成形コードを見てもらえればいいのですが、ひとつ注意する点としては画像マーカーは必ず**Assets.xcassetsの中にAR Resourcesというグループをつくって**登録しなければならないという点です。Assets.xcassetsにそのまま入れた画像をマーカーにしようとしてもうまくいかなかったので注意しましょう。


...さてなんやかんやアニメーションなども適当に加えて、AR系の処理はiPhone 11系でしか実行されないように変えて、完成です。

## 実際にやってみた

さて実際にやってみた動画が[こちら(YouTube)](https://www.youtube.com/watch?v=sszYYVXjNow)になります。

iPhone 7で画像マーカーが出て、それをiPhone 11で見てみると

**そう！iPhone 7とiPhone 11がウーン！（CV：ピコ太郎）ってなってセブンイレブンロゴが出てきました！！！！！！！セブンとイレブンがあわさってセブンイレブン！！！！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10943/ea18a5ad-2ff0-6597-ea30-051ece090eee.png)

......

..........

あ、ごめんなさい。死ぬほど引っ張ってきて結論これです...完全にオヤジギャグです。。。はい。

## つまったところ

今回地味につまったところとしてARSCNViewを動かしたときに以下のWarningが出て画面が真っ暗のままになるという現象がありました。


> 2019-12-19 16:55:33.111128+0900 SevenElevenAR[22084:3756213] [Session] Session (0x113e3e9d0): did fail with error: Error Domain=com.apple.arkit.error Code=102 "Required sensor failed." UserInfo={NSLocalizedFailureReason=A sensor failed to deliver the required input., NSUnderlyingError=0x280329f20 {Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-12780), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x280329740 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}}}, NSLocalizedRecoverySuggestion=Make sure that the application has the required privacy settings., NSLocalizedDescription=Required sensor failed.}

これは[いくつかフォーラムやStackOverflowにも報告されていて](https://stackoverflow.com/questions/45655562/apple-ios-arkit-a-sensor-failed-to-deliver-the-required-input-error-and-stops)、iPhone側の設定でコンパスのキャリブレーションをONにするというものもあるそうなのですが、とりあえずは以下のコードを書くことによって直りました。

```swift
func restartSessionWithoutDelete() {
    arScnView.session.pause()
    arScnView.session.run(imageConfiguration, options: [.resetTracking, .removeExistingAnchors])
}

func session(_ session: ARSession, didFailWithError error: Error) {
    if let arError = error as? ARError {
        switch arError.errorCode {
        case 102:
            imageConfiguration.worldAlignment = .gravity
            restartSessionWithoutDelete()
        default:
            restartSessionWithoutDelete()
        }
    }
}
```

## 締め

というわけで今回は、完全に思いつきからARKitでちょっとしたダジャレアプリをつくってみました。

ハマりどころはたしかにあったものの、どんどんと手軽にこのぐらいのアプリなら作れるようになってきていていい世の中になっていってるなぁという気持ちです。
こちら完成品のコードを置いておくのでぜひ試してみてください笑

- [touyou/SevenElevenAR](https://github.com/touyou/SevenElevenAR)

今日の記事以外にもARKitまわりではいくつか記事を書いているので興味があれば読んでみてください！

- [ARKitとPolyでクリスマスっぽいソシャゲを作ってみた](https://qiita.com/touyoubuntu/items/9280ee7b1b6924c787ec)
- [公式ドキュメントを追いながらARKitを試してみよう](https://qiita.com/touyoubuntu/items/97e481f37a2d775853d7)
- [ARKit+SceneKitで任意のタイミングにモデルを出し入れする方法](https://qiita.com/touyoubuntu/items/8118fbb42422646a8efc)

最近どんどんとAppStoreの審査基準は厳しくなっていますがこんなふうに遊びのアプリだったらどんどん自由な発想で作ってみるのが面白いんじゃないかなと思います。ぜひ面白いものをつくって技術力を上げていきましょう！

### 追伸

友達の面白い記事の雰囲気を参考に書いてみたのですが、Qiitaよりももっと自由に装飾できる媒体でやるべきでしたね。。。笑