Passion+Action+Sincerely=Success!

週末にのみ趣味でコーディングするおやじの備忘録

Swiftのenumが素敵すぎる Part2

前回の投稿で、以下のSwiftの列挙型(enum)がもつ固有の性質のうち、#3と#4を使ってサーチプログラムをリファクタリングした。
今回は、#1と#2を使って、さらにリファクタリングするぜ。

Swiftの列挙型の固有な性質:
1. メンバーに具体的な値をいれる必要はない。メンバーを値として扱える。
2. メンバーに付随する型を設定できる。
3. メンバーに値を入れた場合は、Objective-CのNS_ENUMと同様に扱える。
4. 列挙型自体を値型としてオブジェクトとして扱え、プロパティーやメソッドも定義できる。

問題:
サーチプログラムのコアとなる以下の関数では、サーチ状態を表すのに、大きく3つのパラメタ(isLoading, HasSearched, searchResults)を使っている。なんだけど、本当に興味があるのは、以下の4つの状態のはずで、これらの状態を3つのパラメタ(isLoading, HasSearched, searchResults)の組み合わせで判断する必要がある。コードが美しくないし、わかりにくいので、バグを誘発しそうだ。こういう時に、enumの#1と#2を使ってコードをすっきりできるぜ。

1) サーチ処理実行前
2) サーチ実行中
3) サーチ実行完了かつ該当の項目が一つもない
4) サーチ実行完了かつ該当の項目が一つ以上あり

typealias SearchComplete = (Bool) -> Void

func performSearchForText(text: String, category: Category, completion SearchComplete) { 

  if !text.isEmpty {
    dataTask?.cancel()
    isLoading = true
    hasSearched = true
    searchResults = [SearchResult]()
  
    let url = urlWithSearchText(text, category: category)
  
    let session = NSURLSession.sharedSession()
  
    dataTask = session.dataTaskWithURL(url, completionHandler: {
        data, response, error in 

        var success = false

        if let error = error {
          if error.code == -999 { return } // Search was cancelled
        } else if let httpResponse = response as? NSHTTPURLResponse { 
          if httpResponse.statusCode == 200 {
            if let dictionary = self.parseJSON(data) { 
              self.searchResults = self.parseDictionary(dictionary) 
              self.searchResults.sort(<)
              println("Success! ") 
              self.isLoading = false 
              success = true
            } 
          }
        }
        if (!success) {
           self.hasSearched = false 
           self.isLoading = false
        }
       
        dispatch_async(dispatch_get_main_queue()) {
           SearchCompletion(success)
        }
    })

  dataTask?.resume() }
}

素直に、enumを使えば、以下のように定義できる。これらは、先にのべた本当に興味のある状態を表している。
#1の性質にあるように、4つの状態すべてに値を定義しておらず、それぞれのメンバを値として扱える。
また、4つめの.Resultsのケースが特殊で、#2の性質にあるように、Swiftenumは、メンバーに付随する型を設定できる。ここでは、SearchResultオブジェクトのArrayを設定している。

enum State {
 case NotSearchedYet  // サーチ処理実行前
 case Loading //サーチ実行中
 case NoResults //サーチ実行完了かつ該当の項目が一つもない
 case Results([SearchResult]) //サーチ実行完了かつ該当の項目が一つ以上あり
}

上記の状態(State)に従って、先のPerformSearchForText関数をリファクタリングをしたのが以下である。

func performSearchForText(text: String, category: Category, completion SearchComplete) { 

  if !text.isEmpty {
    dataTask?.cancel()
    
    state = .Loading //サーチ実行中
  
    let url = urlWithSearchText(text, category: category)
  
    let session = NSURLSession.sharedSession()
  
    dataTask = session.dataTaskWithURL(url, completionHandler: {
        data, response, error in 

        self.state = .NotSearchedYet // サーチ処理実行前
        var success = false

        if let error = error {
          if error.code == -999 { return } // Search was cancelled
        } else if let httpResponse = response as? NSHTTPURLResponse { 
          if httpResponse.statusCode == 200 {
            if let dictionary = self.parseJSON(data) { 
              var searchResults = self.parseDictionary(dictionary) 
              if searchResults.isEmpty {
                 self.state = .NoResults //サーチ実行完了かつ該当の項目が一つもない
              } else {
                 searchResults.sort(<)
                 self.state = .Results(searchResults) //サーチ実行完了かつ該当の項目が一つ以上あり
                 success = true
            } 
          }
        }
       
        dispatch_async(dispatch_get_main_queue()) {
           SearchCompletion(success)
        }
    })

  dataTask?.resume() }
}

このようにenumを使えば、処理状態の管理が容易になり、かつ、ある状態にのみ付随する情報をまとめて設定できるので便利だ。
参考に、上記で、.Resultsに付随する情報として設定したsearchResultsを読み出す処理例として、状態(state)に応じて、tableViewの構成する各Cellを返す関数の例を以下に記載しておく。

  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        switch search.state {
        case .NotSearchedYet:
            fatalError("Should never get here")
        case .Loading:
            let cell = tableView.dequeueReusableCellWithIdentifier(TableViewCellIdentifiers.loadingCell, forIndexPath: indexPath) as UITableViewCell
            let spinner = cell.viewWithTag(100) as UIActivityIndicatorView
            spinner.startAnimating()
            
            return cell
            
        case .NoResults:
            return tableView.dequeueReusableCellWithIdentifier(
            TableViewCellIdentifiers.nothingFoundCell, forIndexPath: indexPath) as UITableViewCell
            
        case .Results(let list):
            let cell = tableView.dequeueReusableCellWithIdentifier(
                TableViewCellIdentifiers.searchResultCell, forIndexPath: indexPath) as SearchResultCell
            
            let searchResult = list[indexPath.row]
            cell.configureForSearchResult(searchResult)
            return cell
        }
    }

U