20200103のSwiftに関する記事は9件です。

Swiftのモック生成ライブラリ「Mockolo」のセットアップ&操作方法

はじめに

Xcodeにはモック生成機能が搭載されておらず、手動で実装するのが大変だと感じてきたため、導入することにしました。

「Mockolo」とは?

Swift用のモック生成ライブラリです。

現在はプロトコルのモック生成のみ対応しており、クラスのモック生成は追加予定とのことです。

環境

  • OS:macOS Catalina 10.15.2
  • Swift:5.1.3
  • Xcode:11.3 (11C29)
  • Mockolo:1.1.1

セットアップ

Mockoloのインストール

Mintからインストールします。

Mintfile
+ uber/mockolo@1.1.1
$ mint bootstrap

手動でインストールするには、公式ドキュメントをご参照ください。
https://github.com/uber/mockolo#build--install

ビルド時にモックを生成するようにする

Mockoloはモックの生成時間が速いため、ビルドするたびにモックを生成するようにします。
ビルド時間が気になる場合、手動でコマンドを実行してモックを生成してください。

Xcodeでプロジェクトを開く
TARGETSで製品ターゲットを選択 > Build Phases > +をクリック > New Run Script Phase >
ドラッグ&ドロップで「Compile Sources」の直前に移動

スクリーンショット_2020-01-03_16_43_59.jpg

スクリプトは「Generate Mocks with Mockolo」のようにわかりやすい名前を付けるといいです。

展開して以下のスクリプトを記述します。

if which mint >/dev/null; then
  mint run mockolo mockolo --sourcedirs $SRCROOT/{製品ターゲット名} --destination $SRCROOT/MockResults.swift
else
  echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi

Output Files > +をクリック
--destination で指定しているファイルパスを記述します。

$SRCROOT/MockResults.swift

生成されるファイルを記述しないと、CI/CD時に以下のエラーが発生します。

error: Build input file cannot be found:

以下の記事を参考にさせていただきました。
https://qiita.com/lovee/items/fa3ef5e60cfbf31996c0

スクリーンショット_2020-01-04_12_16_45.jpg

Mintを使っていない場合、 mint run mockolo を外し、if文の条件を変更してください。

使っているオプションを説明します。
以下の2つは必須であり、必要に応じて値を変更してください。

オプション 説明
--sourcedirs 生成対象のフォルダパス
製品ターゲット名のフォルダを指定すれば、通常は全ファイルを対象にできる
--destination モックの生成パス

その他のオプションは公式ページまたは mockolo --help をご参照ください。

プロジェクトをビルドし、「$SRCROOT(通常はプロジェクトのルートフォルダ)」に「MockResults.swift」が生成されたら、プロジェクトにドラッグ&ドロップします。
スクリーンショット_2020-01-03_16_53_20.jpg

[Copy items if needed]チェックをOFFにし、[Finish]をクリックします。
スクリーンショット_2020-01-03_16_51_53.jpg

バージョン管理から無視する

不要な競合を防ぐため、生成された「MockResults.swift」をバージョン管理の対象外にします。

Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。

.gitignore
MockResults.swift

操作方法

モックを生成したいプロトコルに @mockable のドキュメンテーションコメントを付けます。
タイプエイリアスがある場合、カッコ内に書きます。

Foo.swift
/// @mockable(typealias: T = AnyObject; U = StringProtocol)
public protocol Foo {
    associatedtype T
    associatedtype U: Collection where U.Element == T 
    associatedtype W 

    var num: Int { get set }

    func bar(arg: Float) -> String
}

ビルドすると、モックが生成されます。

MockResults.swift
// クラス名は `{プロトコル名}Mock` となる
public class FooMock: Foo {
    typealias T = AnyObject
    typealias U = StringProtocol
    typealias W = Any // 指定しないと `Any` になる

    init() {}
    init(num: Int = 0) {
        self.num = num
    }

    var numSetCallCount = 0
    var underlyingNum: Int = 0
    var num: Int {
        get {
            return underlyingNum
        }
        set {
            underlyingNum = newValue
            numSetCallCount += 1
        }
    }

    var barCallCount = 0
    var barHandler: ((Float) -> (String))?
    func bar(arg: Float) -> String {
        barCallCount += 1
        if let barHandler = barHandler {
            return barHandler(arg)
        }
        return ""
    }
}

自動生成されたコードのうち、テストで使うプロパティのみ説明します。

プロパティ 説明
{プロパティ名}SetCallCount セッターの呼び出し回数
{メソッド名}CallCount メソッドの呼び出し回数
{メソッド名}Handler メソッドの呼び出し時に実行されるクロージャ

テスト時は以下のように使います。

FooTests.swift
func testMock() {
    // モックを生成する
    let mock = FooMock(num: 5)

    // 対象プロパティのセット回数を確認する
    XCTAssertEqual(mock.numSetCallCount, 1)

    // ハンドラは対象メソッドの呼び出し前に自分で代入する
    mock.barHandler = { arg in
        return String(arg)
    }

    // 対象メソッドの呼び出し回数を確認する
    XCTAssertEqual(mock.barCallCount, 0)
}

おわりに

とても簡単にモックを生成できました!
これでテスト時にVIPERのモックを手動で実装する手間が省けるぞ?

もっと早く導入すればよかったと思いました笑

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】三項演算子をswitch文に置き換えたら読みやすくなった

三項演算子はよく使いますが、条件が増えるとわかりづらくなるという難点があります。
そこで、switch文にしたら読みやすくなったという話です。

もともとのコード

三項演算子
let condA = true

let v = condA ? 1 : 2

条件が一つしかない場合は、三項演算子を使うとわかりやすいコードになります。

ところが、ここに新しい条件CondBが増えると、

条件が2つある場合
let condA = true
let condB = false

let w = condA ? (condB ? 1 : 2) : 3

このように入れ子になってしまいわかりづらくなります。

switch文を使って書き換えてみる

上のコードをswitch文で書き換えてみます。

switch文を使った場合
var x:Int
switch(condA, condB) {
case (true,true):
    x = 1
case (true,false):
    x = 2
case (false,true), 
     (false,false):
    x = 3
}

condAcondBの状態ごとにどのような値になるかが一目瞭然です。

さらに改良してみる

上のコードをさらに改良して、代入式にまとめてみます。

改良をしたコード
let y = { condA, condB -> Int in
    switch(condA, condB) {
    case (true,true):
        return 1
    case (true,false):
        return 2
    case (false,true),
         (false,false):
        return 3
    }
}(condA, condB)

このようにすることで、CondACondBを入力としてyの値を定める、という全体の流れを表現することができるので読みやすくなります。

まとめ

三項演算子を使っていて条件が増えた場合、switch文を使ったら条件が直感的に表現されたので読みやすいコードになりました。また、switch文は条件のすべてを列挙する必要があるため、複数の条件があったときに考慮もれの防止にも繋がります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モンテカルロ法でルーレットに賭け続けた場合に本当に勝てるのか調べた話

初めに

モンテカルロ法 というギャンブルの戦略があることを知り、実際にこれで勝負した場合の勝率がどの程度になるのかプログラムを書いて調べてみた。

参考動画: https://www.youtube.com/watch?v=_suZM2y9wyA

モンテカルロ法とは

勝率1/2の勝負に、以下のルールでベットを行い続ける方法

まず [1, 2, 3] の配列を作成する

-- loop --

  1. 配列の要素数が2未満だったらループを抜ける
  2. 配列の先頭と末尾の要素を足した額をベットする (初期状態では 4 )
  3. 勝ったら → 配列の先頭と末尾の要素を削除し 1. に戻る。
    負けたら → 配列の末尾に2.でベットした額と同じ値を追加し 1. に戻る

-- loop end --

書いたコード

https://github.com/yamazaki-sensei/montecarlo

  • ルーレットで赤or黒に賭けるのと同様の場合を想定。
  • 0だと赤でも黒でも外れ。
  • 予算は100000単位
  • プログラムでは10000ループのモンテカルロ法を行う
import Foundation

let roulette = (0...36) // 疑似ルーレット

func spin() -> Bool {
    return roulette.randomElement()! >= 19 // 0から18の数字が出たら負け
}

let n = 100
var budget = 100_000

let initialState = [1, 2, 3]

(1...100).forEach { _ in
    assert(budget > 0, "破産")
    (1...n).forEach { _ in
        var state = initialState
        while state.count >= 2 {
            if state.last! > 1000 {
                print(state)
            }
            let toBet = state.first! + state.last!

            if spin() {
                state.removeFirst()
                state.removeLast()
                budget += toBet
            } else {
                budget -= toBet
                state.append(toBet)
            }
        }
    }

    print("#################")
    print(budget)
}

所感

  • 10000ループだと80%くらいの確率で勝つ気がする。
  • 勝った時の儲けはだいたい10000単位くらい。
  • 負けるときは大負けして最終的なbudgetが10000とかになる → このときのstateを覗いてみると、 state.last! が10000以上とかになっている。怖い。
  • コードを変えて100000ループ回すと、30%くらいの確率で破産する。怖い。

終わりに

実世界の場合一度破産したら死んでしまうので恐ろしくてなかなか出来ない。やはり必勝法などは無かった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Composite~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Compositeパターン概要

  • ディレクトリとファイルのような、ツリー構造を扱うためのパターンです。
  • ディレクトリとファイルのように、「名前」などの同じプロパティや、「削除」などの同じ操作を持つ場合、ディレクトリ(容器)とファイル(中身)を同じように扱うことができます。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • そのものズバリ、ツリー構造を扱う場合には、Compositeパターンを思い浮かべると設計に掛かる時間を短縮できる可能性が高いです。
  • UIViewのView Hierarchyもツリー構造であり、Compositeパターンが使われています。

view_hierarchy.gif
引用:Cocoa Design Patterns (Retired Document)

サンプルコード

Swiftバージョンは 5.1 です。

protocol DirectoryEntry {
    var name: String { get }
    func remove()
}

final class File: DirectoryEntry {
    let name: String

    init(name: String) {
        self.name = name
    }

    func remove() {
        print("\(name)を削除しました")
    }
}

final class Directory: DirectoryEntry {
    let name: String
    private var entryList = [DirectoryEntry]()

    init(name: String) {
        self.name = name
    }

    func add(entry: DirectoryEntry) {
        entryList.append(entry)
    }

    func remove() {
        for entry in entryList {
            entry.remove()
        }
        print("\(name)を削除しました")
    }
}

// Usage
let dir1 = Directory(name: "dir1")
let file1 = File(name: "file1")
dir1.add(entry: file1)
// dir1
//   ∟file1

let dir2 = Directory(name: "dir2")
let file2 = File(name: "file2")
let file3 = File(name: "file3")
dir2.add(entry: file2)
dir2.add(entry: file3)
// dir2
//   ∟file2
//   ∟file3

dir1.add(entry: dir2)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3

let file4 = File(name: "file4")
dir1.add(entry: file4)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3
//   ∟file4

dir1.remove()
// "file1を削除しました"
// "file2を削除しました"
// "file3を削除しました"
// "dir2を削除しました"
// "file4を削除しました"
// "dir1を削除しました"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftのTableViewCellを使ってTableViewを自由にカスタマイズ

概要

 この記事は初心者の自分がRESTful なAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。

前回の swiftで配列型のJSONから値を取り出す でクーポンの詳細情報や利用可能店舗名、有効期間の情報を取得できるようになりましたので、それらをアプリ画面に表示するようにTableViewを改造します。

参考

  • やさしくはじめる iPhoneアプリ作りの教科書 森 巧尚 著 マイナビ

環境

Mac OS 10.15
Swift5
Xcode11.1

手順

  • Main.storyboardでTableViewCellをデザイン
  • 配置したラベルにそれぞれの情報を表示できるようにViewControllerを改造
  • 動作確認

Main.storyboardでTableViewCellをデザイン

 現在は subtitle というTableViewの標準のデザインを使い、メイン情報を詳細情報の2つを表示する仕様になっています。3つ以上の情報をtableViewに表示したい場合は標準デザインが対応していないためオリジナル仕様のTableViewCellを作る必要があります。

 まず既に配置済みのTableViewの上にTableViewCellを配置します。ObjectsのウィンドウはXcodeの右上の「+」ボタンで表示できます。
put-tableview-tableviewcell.png

配置したTableViewCellのIdentifier にTableViewCellを特定するための名前をつけます。
set-identifier.png

右側の「show the size inspector」でTableViewCellの高さを200ptに設定し、TableViewCellの上に、クーポンの情報を表示するためのLabelを配置しました。ラベルの用途は下記の通りです。

  • クーポン特典を表示するラベル
  • クーポンのコメントを表示するラベル
  • 利用可能店舗を表示するラベル
  • 開始利用開始日と利用期限を繋げて利用可能期間を表示するラベル

それぞれのラベルをプログラムから識別するためにtag(通し番号)をつけます。
set-tag-to-labels-mark.png

タグ番号の振り分けは下記の通りにしました。

  1. クーポン特典を表示するラベル
  2. クーポンのコメントを表示するラベル
  3. 利用可能店舗を表示するラベル
  4. 開始利用開始日と利用期限を繋げて利用可能期間を表示するラベル

クーポン特典とコメントは複数行で表示させたいので行数を2行に設定します。LabelのLinesで行数を設定できます。
set-label-lines-2-mask.png

以上でTableViewCellのデザインは完了です。

配置したラベルにそれぞれの情報を表示できるようにViewControllerを改造。

「10_swiftでクーポンを表示する画面を実装する」で作成したプログラムを改造していきます。
改造する箇所は関数func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)だけです。

まず、セルオブジェクトを作るコードをMain.storyboard でデザインしたCell(couponCell)のセルオブジェクトを作る形式に変えます。

変更前

let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "couponCell")

変更後

withIdentifier: のところは 作成したTableViewCellのIdentifierに設定した名前(今回は couponCell)を指定します。

let cell = tableView.dequeueReusableCell(withIdentifier: "couponCell", for: indexPath as IndexPath)

次に、セルの高さを設定します。(Main.storyboardでも設定しましたが、ViewControllerでも設定しないと反映しませんでした。)

tableView.rowHeight = 200

次に各Labelに表示する値を設定します。
ラベルオブジェクトを作るところで、cell.viewWithTag( ) の「( )」に設定したtagの通し番号を入力します。ラベルに表示する文字列を設定する部分で、変数coupon[String: Any]型なので、String型にキャストします。コードは以下のようになります。

//ラベルオブジェクトを作る
let labelBenefit = cell.viewWithTag(1) as! UILabel
//ラベルに表示する文字列を設定
labelBenefit.text = (coupon["coupon_benefits"] as! String)

これを4つのラベル分、実装します。

出来上がったコードはこちらです。

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let coupon = self.coupons[indexPath.row]
        //セルを作る
        let cell = tableView.dequeueReusableCell(withIdentifier: "couponCell", for: indexPath as IndexPath)

        tableView.rowHeight = 200

         //各ラベルに値を設定する
        let labelBenefit = cell.viewWithTag(1) as! UILabel
        labelBenefit.text = (coupon["coupon_benefits"] as! String)

        let labelExplanation = cell.viewWithTag(2) as! UILabel
        labelExplanation.text = (coupon["coupon_explanation"] as! String)

        let labelStore = cell.viewWithTag(3) as! UILabel
        labelStore.text = (coupon["coupon_store"] as! String)

        let labelDay = cell.viewWithTag(4) as! UILabel
        labelDay.text = "有効期間: " + (coupon["coupon_start"] as! String) + " ~ " + (coupon["coupon_deadline"] as! String)

動作確認

実行したところ、デザインした通りにクーポンが表示されています。
app-test-14.png

これで少しクーポンらしくなりました。

次回は、webAPI側をRESTfulなAPIに改造します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swiftで配列型のJSONから値を取り出す

概要

 この記事は初心者の自分がRESTfulなAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。

 前回のDBに格納したデータを返すwebAPIをDjangoとSQLiteで開発するでWebAPIの仕様を変えたので、新しいAPIの仕様に合わせて、swiftでwebAPIを呼び出してjsonデータを表示させる で作成したswiftのコードを改造します。大きな変更点は、レスポンスのjsonオブジェクトが配列になった点です。これにより複数のクーポン情報が取得出来るようになりました。

これまでのjson (例)

{“name”:”beer”,”price”:500}

新しいjson (例)

[
    {“name”:”beer”,”price”:500}
    {“name”:”vodka”,”price”:700}
    {“name”:”scotch”,”price”:800}
]

jsonのデータを取り出してパースする処理を配列型に対応させる必要があります。

参考

環境

Mac OS 10.15
Swift5
Xcode11.1

手順

  • レスポンスの内容をData型からjson形式に変換しコンソールで確認する
  • jsonをAny型にキャストする
  • 配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする

レスポンスのjsonの形式を確認する

 webAPI側はjsonをオブジェクトの配列で返す仕様になっていますが、念のためどの様なjsonが返ってくるか確認します。

まずはブラウザでAPIのURLにアクセスしてjsonの形式を確認します。
check-api-response-by-safari-1200.png

次にターミナルでも確認してみます。ターミナルの場合は curl コマンドで APIのURLにアクセスします。
check-api-response-by-curl-mask.png

curlコマンドを使う方が改行がされていて確認しやすいですね。もっと複雑なJSONの確認をする場合は専用のツールを使うの方法があるそうです。

想定した通り、配列型の下記の様な形式のjsonになっています。

[
  {...}
  {...}
  {...}
]

次にXcodeでData型をjsonに変換しただけのデータをコンソールに出力させてみます。swiftでwebAPIを呼び出してjsonデータを表示させる で作成したコードを使う場合は、ViewController.swiftviewDidLoadの一部を下記の通りコメントアウトや修正します。

  • APIのURLをhttp://127.0.0.1:8000/coupon/に変更
  • JSONSerialization〜の後ろの as! [String: Any]は型違いでエラーになるので削除
  • TableViewとデータをやり取りするためのグルーバル変数に代入する処理をコメントアウト

修正後のviewDidLoadはこちらです。

ViewController.viewDidLoad
    override func viewDidLoad() {
        super.viewDidLoad()


        let url: URL = URL(string: "http://127.0.0.1:8000/coupon/")! // URLの変更
        let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
            do  {
                let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
 print(couponData)

/*  ここはコメントアウト                               
                DispatchQueue.main.async() { () -> Void in
                    self.couponBenefit = couponData["coupon_benefits"] as! String
                    self.couponDeadline = couponData["coupon_deadline"] as! String
                    }
*/

                }
            catch {
                print(error)
                }
        })
        task.resume()
    }

この状態で実行してコンソールに出力されるjsonを確認します。すると、下記の様に出力されます。

check-api-response-by-console.png

ブラウザやcurlで表示されるjsonは外側のかっこが[ ]ですが、アプリ上ではかっこが( )になっています。このままでは配列として扱えず、中のデータが取り出せません。そこで、Any型にキャストします。

JSONをAny型にキャストする

JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)の後ろに as! [Any] を付けるだけです。修正前のコードではas! [String: Any]が付いていたところです。

do  {
     let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any] //Any型にキャスト
     print(couponData)

            //...中略...

     }
     catch {
         print(error)
         }

すると、コンソールに出力されたjsonの外側のかっこが( )から[ ]に変わりました。これで配列として扱えるようになりました。
check-api-resopnse-after-cast.png

配列に格納されているjsonのオブジェクトを一つずつ String型にキャストする

 jsonクラスのmapという関数を使い、jsonの配列のオブジェクトに対して[String: Any]型にキャストしたものを新しい配列couponDataに格納するようにします。

JSONSErializationの処理の下に以下の処理を追加します。

let couponData = couponDataArray.map { (couponData) -> [String: Any] in
    return couponData as! [String: Any]
}

ここまでの処理で個々のjsonオブジェクトの情報を取り出せるようになりました。情報を取り出す際、変数couponDataはjsonオブジェクトと、オブジェクトに格納されているモデルフィールドの二重配列となっているので、
個々のデータは、couponData[オブジェクトの番号(0~)][“モデルフィールド名(jsonのキーと同じ)”]で指定できます。

但しそのままだと[String: Any]型なので、取り出すモデルフィールドの型に合わせてキャストします。文字型の場合はas! Stringでキャストします。下記のようなコードでデータを取り出していきます。

couponData[オブジェクトの番号(0~)][“JSONのキー名”] as! [モデルフィールドの型]

試しに以下のコードを追加し、クーポン2つ分のクーポン特典をコンソールに出力させてみます。

print(couponData[0]["coupon_benefits"] as! String)
print(couponData[1]["coupon_benefits"] as! String)

実行してコンソールを見ると、クーポン2つ分のクーポン特典が出力されているので処理が成功しています。
out-put-data-by-2objects.png

以上でオブジェクトの配列形式のjsonから個々の情報を取り出せるようになりました。

複数のクーポン情報をTableViewで表示するように改造する。

 複数のクーポンの情報をレスポンス出来るようになったので、TableViewに複数のクーポンの情報を表示できるようにします。

まず、TableViewにデータを渡す方法を変え、jsonオブジェクトの配列を[String: Any]型にパースした 変数couponDataをそのままTableViewに渡すようにします。そのためメンバー変数の定義部分を下記のように修正します。

  • メンバー変数の型を[String: Any]型にし、couponDataをそのまま格納出来るようにする
  • オブジェクトの項目毎のメンバ変数は不要なので削除
    var coupons: [[String: Any]] = [] { //パースした[String: Any]型のクーポンデータを格納するメンバ変数
        didSet{
            tableView.reloadData()
        }
    }

次にテーブルの行数を渡している部分を改造します。配列のcountメソッドを使ってデータを格納するメンバ変数(coupons)の配列の行数を渡すようにします。

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.coupons.count
    }

次にテーブルに表示するデータを渡している部分を改造します。

辞書型変数のcouponを定義し、couponsの配列の1行(jsonの{...}で囲まれたオブジェクト)ごとにcouponに格納します。そして辞書型のcouponからモデルフィールドの名前(jsonのキー)でデータを取り出し、データをString型にキャストしてセルに渡します。

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "couponCell")
        //辞書型変数のcouponを定義
        let coupon = self.coupons[indexPath.row]

        //モデルフィールドの名前でデータを取り出し、String型にキャストしてセルに渡す
        cell.textLabel?.text = (coupon["coupon_benefits"] as! String)
        cell.detailTextLabel?.text = "有効期限:" + (coupon["coupon_deadline"] as! String)

        return cell
    }

最後にviewDidLoadでjsonをパースしてメンバ変数に格納する部分を改造します。

String型の変数にjsonオブジェクト内のデータを直接格納していましたが、[String: Any]型のままTableViewに渡す仕様となったため、jsonをパースした[String: Any]型のデータをそのままメンバ変数couponsに渡すようにします。

      DispatchQueue.main.async() { () -> Void in
          self.coupons = couponData
      }

アプリを起動すると、先にjsonで確認した3件つのクーポンがTableViewで表示されるようになりました。
test-ami-coupon-02.png

修正後のViewControllerクラスは下記の通りです。

ViewController.swift
import UIKit

class ViewController: UIViewController,UITableViewDataSource,UITableViewDelegate {
    @IBOutlet weak var tableView: UITableView!

    var coupons: [[String: Any]] = [] { //パースした[String: Any]型のクーポンデータを格納するメンバ変数
        didSet{
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let url: URL = URL(string: "http://127.0.0.1:8000/coupon/")!
        let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
            do  {
                let couponDataArray = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
                let couponData = couponDataArray.map { (couponData) -> [String: Any] in
                    return couponData as! [String: Any]
                }

                DispatchQueue.main.async() { () -> Void in
                    self.coupons = couponData
                    }
                }
            catch {
                print(error)
                }
        })
        task.resume()
    }


    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.coupons.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "couponCell")
        //辞書型変数のcouponを定義
        let coupon = self.coupons[indexPath.row]

        //モデルフィールドの名前でデータを取り出し、String型にキャストしてセルに渡す
        cell.textLabel?.text = (coupon["coupon_benefits"] as! String)
        cell.detailTextLabel?.text = "有効期限:" + (coupon["coupon_deadline"] as! String)

        return cell
    }

}

以上です。

次回は TableViewを改造して、取得したクーポン情報をアプリ画面に表示出来るようにします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Singleton~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Singletonパターン概要

  • インスタンスが1個しか生成されないことを保証するためのパターンです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

  • iOSフレームワーク内で随所に使われています。典型例は UIApplication.shared です。
  • 実行時に依存性を変更しづらく、ユニットテストがしにくいというデメリットがあります。
  • またグローバル変数と同様に、いつ・どこから・どのように変更されるのか理解が難しくなりますので、注意しながら利用する必要があります。

サンプルコード

Swiftバージョンは 5.1 です。

class SingletonClass {
    class var shared : SingletonClass {

        struct Static {
            static let instance : SingletonClass = SingletonClass()
        }

        return Static.instance
    }
}

// Usage
let instance = SingletonClass.shared

※少々回りくどい実装になっていますが、マルチスレッドでインスタンスが複数生成されてしまうことを回避するための実装例です。

引用:
https://github.com/07cs07/Design-Patterns-In-Swift

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「リファクタリング 第2版」Swiftでコーディング その15

21-22頁 第1章 ボリューム特典ポイント集計箇所の削除 totalAmount変数の削除

Swift版 main.swift

データ生成、結果表示付き。

import Foundation

makeData()

func playFor(aPerformance:Performance) -> Play {
    return plays[aPerformance.playID]!
}

func volumeCreditsFor(aPerformance:Performance) -> Int {
    var result = 0
    result += max(aPerformance.audience - 30, 0)
    if "comedy" == playFor(aPerformance: aPerformance).type {
        result += Int(aPerformance.audience / 5)
    }
    return result
}

func usd(aNumber:Int) -> String {
    let format = NumberFormatter()
    format.numberStyle = .currency
    format.locale = Locale(identifier: "en_US")
    return format.string(from: NSNumber(value: aNumber / 100))!
}

func totalVolumeCredits(invoice:Invoice) -> Int {
    var result = 0
    for perf in invoice.performances {
        result += volumeCreditsFor(aPerformance: perf)
    }
    return result
}

func totalAmount(invoice:Invoice) -> Int {
    var result = 0
    for perf in invoice.performances {
        result += amountFor(aPerformance: perf)
    }
    return result
}

func statement(invoice:Invoice, plays:Dictionary<String, Play>) -> String {
    var result = "Statement for \(invoice.customer)\n"

    for perf in invoice.performances {
        result += "  \(playFor(aPerformance: perf).name): " + usd(aNumber: amountFor(aPerformance: perf)) + " (\(perf.audience) seats)\n"
    }
    result += "Amount owed is " + usd(aNumber: totalAmount(invoice: invoice)) + "\n"
    result += "You earned \(totalVolumeCredits(invoice: invoice)) credits\n"
    return result
}

func amountFor(aPerformance:Performance) -> Int {
    var result = 0

    switch playFor(aPerformance: aPerformance).type {
    case "tragedy":
        result = 40000
        if aPerformance.audience > 30 {
            result += 1000 * (aPerformance.audience - 30)
        }
    case "comedy":
        result = 30000
        if aPerformance.audience > 20 {
            result += 10000 + 500 * (aPerformance.audience - 20)
        }
        result += 300 * aPerformance.audience
    default:
        print("error")
    }
    return result
}

let result = statement(invoice: invoices[0], plays: plays)
print(result)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「リファクタリング 第2版」Swiftでコーディング その14

18-20頁 第1章 ボリューム特典ポイント集計箇所の削除 「ループの分離(p.236)」「ステートメントのスライド(p.231)」「問い合わせによる一時変数の置き換え(p.185)」「関数の抽出(p.112)」

Swift版 main.swift

データ生成、結果表示付き。

import Foundation

makeData()

func playFor(aPerformance:Performance) -> Play {
    return plays[aPerformance.playID]!
}

func volumeCreditsFor(aPerformance:Performance) -> Int {
    var result = 0
    result += max(aPerformance.audience - 30, 0)
    if "comedy" == playFor(aPerformance: aPerformance).type {
        result += Int(aPerformance.audience / 5)
    }
    return result
}

func usd(aNumber:Int) -> String {
    let format = NumberFormatter()
    format.numberStyle = .currency
    format.locale = Locale(identifier: "en_US")
    return format.string(from: NSNumber(value: aNumber) / 100)!
}

func totalVolumeCredits(invoice:Invoice) -> Int {
    var volumeCredits = 0
    for perf in invoice.performances {
        volumeCredits += volumeCreditsFor(aPerformance: perf)
    }
    return volumeCredits
}

func statement(invoice:Invoice, plays:Dictionary<String, Play>) -> String {
    var totalAmount = 0
    var result = "Statement for \(invoice.customer)\n"

    for perf in invoice.performances {
        result += "  \(playFor(aPerformance: perf).name): " + usd(aNumber: amountFor(aPerformance: perf)) + " (\(perf.audience) seats)\n"
        totalAmount += amountFor(aPerformance: perf)
    }

    result += "Amount owed is " + usd(aNumber: totalAmount) + "\n"
    result += "You earned \(totalVolumeCredits(invoice: invoice)) credits\n"
    return result
}

func amountFor(aPerformance:Performance) -> Int {
    var result = 0

    switch playFor(aPerformance: aPerformance).type {
    case "tragedy":
        result = 40000
        if aPerformance.audience > 30 {
            result += 1000 * (aPerformance.audience - 30)
        }
    case "comedy":
        result = 30000
        if aPerformance.audience > 20 {
            result += 10000 + 500 * (aPerformance.audience - 20)
        }
        result += 300 * aPerformance.audience
    default:
        print("error")
    }
    return result
}

let result = statement(invoice: invoices[0], plays: plays)
print(result)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む