[Swift]画像の非同期処理で使ってたoptional型でハマった。


今回、今まで発生しなかった事象とそれの解決に至るまでのデバッグを
同様の事象が起きた際にスピード感を持って解決出来るように纏めてみました。


## 事象
- [deploy gate](https://deploygate.com/)で配布したアプリを確認中に今まで正常に起動していた主要機能のほぼ全てにおいて急に落ちる事象が発生。

## 状態
- 落ちる場合と落ちない場合の差分が分かりづらく、規則性も無い状態。
- iOS7,iOS8などのOSによる問題はなくどちらでも事象が発生する。


## 解決までの所要時間
約1h 

## デバッグ
 1. 事象の再現確認
   - [deploy gate](https://deploygate.com/)で配布したアプリのみ起きる事象かそうでないのかの事象切り分けを行う。
   - 開発環境でも再現することを確認。
   - どの事象で発生するのか事象の洗い出し。大体、10パターンまで出た段階でSTOP。


 2. 開発環境でログ確認
   - 下記のエラーメッセージが表示されることを確認。
   - 直前まで出力していたログからURL、image_pathに問題ないか確認。
   - この時点でnot foundになる画像とそうでない画像があるところまでしか分かっていない。
   - 原因はimage_pathがnot foundになった場合の実装漏れと特定。
   - この時点で画像の呼び出し元は画像の非同期処理であることを確認。

       
 3. 画像の非同期処理が原因であったため、optional型を集中的に調査
   - この時点ではoptional型の「let cache = UIImage(data: data)!」が原因というところまでしか分かってない。
   - ここからググり始める。
   - optional型に関する情報にはhitし、試してみるが事象が解決されない。
   - ググるのを止めてoptional型の理解へシフト。
   - 原因を特定。

4.コード修正

   - optional型であるlet cacheが原因と分かり、それに準じた内容で対応。詳細は後述の修正前のソース、修正後のソースを参照。


5.事象が解決されたか確認

   - 開発環境で事象が解決されたことを確認。
   - [deploy gate](https://deploygate.com/)で再配布し事象が解決されたことを確認。




## エラーメッセージ

```
fatal error: unexpectedly found nil while unwrapping an Optional value
```

## エラーメッセージの画面キャプチャ(コンソール部分のみ抽出)

![スクリーンショット 2015-04-18 18.19.24.png](https://qiita-image-store.s3.amazonaws.com/0/15812/382c487b-e84a-ac15-d405-3dac08498da9.png "スクリーンショット 2015-04-18 18.19.24.png")




### 非同期側の処理

```Utility.swif
// 非同期処理のラッパー
struct Async {
    // 非同期スレッドで実行
    static func runAsyncThread(block: () -> ()) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block)
    }

    // メインスレッドで実行
    static func runMainThread(block: () -> ()) {
        dispatch_async(dispatch_get_main_queue(), block)
    }
}
```

## 修正前のソース：

### 非同期の呼び出し側の処理

````ImageManager.swift
class ImageManager {
    class var sharedInstance: ImageManager {
        struct Singleton {
            static let instance = ImageManager()
        }

        return Singleton.instance
    }

    // 画像キャッシュ用配列
    private var imageCache: Dictionary<String, UIImage> = [:]

    // 画像を取得してキャッシュする
    func fetch(imageURL: String, callback: UIImage -> ()) {
        Utility.debug("string\(imageURL)")
        let noImage = "http://g08m11.co.jp/images/noimage.jpg"
        // キャッシュが存在する場合
        if let cache = self.imageCache[imageURL] {
            Async.runMainThread({
                callback(cache)
            })

            // キャッシュが存在しない場合
        } else {
            if let imgURL = NSURL(string: imageURL) {

                let request: NSURLRequest = NSURLRequest(URL: imgURL)

                Async.runAsyncThread({
                    NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!, error: NSError!) -> Void in
                        if error == nil {
                            let cache = UIImage(data: data)!

                            // Store the image in to our cache
                            self.imageCache[imageURL] = cache

                            Async.runMainThread({
                                callback(cache)
                            })
                        } else {
                            Utility.debug("Image Cache Error: \(error.localizedDescription)")
                        }
                    })
                })

            } else {
                Utility.debug("Image Cache Error: URL is nil")
                let noImageCache = self.imageCache[noImage]

                Async.runMainThread({
                    callback(noImageCache!)
                })
            }
        }
    }
}

````

## 修正後のソース
### 非同期の呼び出し側の処理

````ImageManager.swift
class ImageManager {
    class var sharedInstance: ImageManager {
        struct Singleton {
            static let instance = ImageManager()
        }
        
        return Singleton.instance
    }
    
    // 画像キャッシュ用配列
    private var imageCache: Dictionary<String, UIImage> = [:]
    
    // 画像を取得してキャッシュする
    func fetch(imageURL: String, callback: UIImage -> ()) {
        let noImage = "http://g08m11.co.jp/images/noimage.jpg"
        // キャッシュが存在する場合
        if let cache = self.imageCache[imageURL] {
            Async.runMainThread({
                callback(cache)
            })
            
            // キャッシュが存在しない場合
        } else {
            if let imgURL = NSURL(string: imageURL) {
                Utility.debug("string\(imageURL)")
                
                let request: NSURLRequest = NSURLRequest(URL: imgURL)
                
                Async.runAsyncThread({
                    NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!, error: NSError!) -> Void in
                        if error == nil {
                            if let cache = UIImage(data: data){
                                // Store the image in to our cache
                                self.imageCache[imageURL] = cache
                                
                                Async.runMainThread({
                                    callback(cache)
                                })
                                
                            }
                            
                        } else {
                            Utility.debug("Image Cache Error: \(error.localizedDescription)")
                        }
                    })
                })
                
            } else {
                Utility.debug("Image Cache Error: URL is nil")
                let noImageCache = self.imageCache[noImage]
                
                Async.runMainThread({
                    callback(noImageCache!)
                })
            }
        }
    }
}


````

## 修正点抜粋

### 修正前

```
                        if error == nil {
                            let cache = UIImage(data: data)!

                            // Store the image in to our cache
                            self.imageCache[imageURL] = cache

                            Async.runMainThread({
                                callback(cache)
                            })
                        } else {
                            Utility.debug("Image Cache Error: \(error.localizedDescription)")
                        }
                    })
                })


```

### 修正後

````

                        if error == nil {
                            if let cache = UIImage(data: data){
                                // Store the image in to our cache
                                self.imageCache[imageURL] = cache
                                
                                Async.runMainThread({
                                    callback(cache)
                                })
                                
                            }
                            
                        } else {
                            Utility.debug("Image Cache Error: \(error.localizedDescription)")
                        }
                    })
                })


````

要は「let cache = UIImage(data: data)!」と決め打ちで入れるのではなくクロージャを使ったという修正になります。



## 課題
 - 現時点ではnot foundの画像に対してno_imageなどの画像表示対応ができておらず空白表示になってしまい、ユーザビリティを下げてしまう。。。


## 学び
 - optional型のnilケースはnilそのままはもちろんレスポンスでもnilになるケースを考慮して使う必要がある。
 - 今回のケースだけでなく正常に機能することが確認済(テスト済)でも時間が経てばバグったりすることを念頭において開発を進める。
 - デバッグを言語化することによって自分の思考が少し整理された気がする。(まだまだ改善の余地があるのでもっとショートカットしていきたい。)


<br>

同様のロジックで画像の非同期処理の対応をされている方の参考になれば幸いです。