20200101のSwiftに関する記事は8件です。

オブジェクト指向は iPhone アプリ開発で、どのように役立っているか

はじめに

先日、
BASIC から来た手続き型おじさんが、「手続き型」「関数型」「オブジェクト指向」について考える。
というタイトルで投稿しました。
この中で、

swift で iPhone アプリを開発する時は、オブジェクト指向を特に意識しなくても存分にオブジェクト指向の恩恵を受けています。

と述べました。
本稿では私が実際に App Store に公開しているアプリで解説します。

肝硬変重症度Child分類

これは消化器内科・外科医向けのアプリ*で肝硬変重症度 Child 分類が簡単に算出できます。
2010年に公開して以来、4万人以上の方々にダウンロードして頂きました。
総ビリルビンやアルブミンなどの値を選択すると Child 分類と点数が表示されます。
以下がスクリーンショットとソースコードです。
ChildSCRN.gif

import UIKit

class ViewController: UIViewController {

    @IBOutlet var bilirubin:UISegmentedControl!
    @IBOutlet var albumin:UISegmentedControl!
    @IBOutlet var pt:UISegmentedControl!
    @IBOutlet var fukusui:UISegmentedControl!
    @IBOutlet var nosho:UISegmentedControl!
    @IBOutlet var childIndex:UILabel!

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

    @IBAction func selectorChanged(){
        let b = bilirubin.selectedSegmentIndex + 1
        let a = albumin.selectedSegmentIndex + 1
        let p = pt.selectedSegmentIndex + 1
        let f = fukusui.selectedSegmentIndex + 1
        let n = nosho.selectedSegmentIndex + 1
        let childPoint = b + a + p + f + n
        var childGrade = "A"
        switch childPoint {
        case 5 ... 6:
            childGrade = "A"
        case 7 ... 9:
            childGrade = "B"
        case 10 ... 15:
            childGrade = "C"
        default:
            break
        }
        childIndex.text = "Child \(childGrade)  \(childPoint)点"
    }
}

わずか30行足らずのコードです。
bilirubin, albumin, pt, fukusui, nosho などは UISegmentedControl クラスのインスタンスで childIndex は UILabel クラスのインスタンスです。
我々開発者は UISegmentedControl クラスや UILabel クラスの内部ロジックを知らなくても使うことができ(隠蔽)、 UISegmentedControl クラスや UILabel クラスの内部を書き換えることはありません(カプセル化)。

おわりに

クラス化された UI 部品(オブジェクト)を組み合わせることで、私のようなアマ・プログラマーでも iPhone アプリを作ることができるのはオブジェクト指向の恩恵だと思います。

*医師向けアプリです。
肝硬変重症度Child分類・消化器癌進行度分類

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

SwiftUI: ListとIdentifiableについて

はじめに

今回はListとIdentifiableプロトコルを実装した構造体を利用してUITableViewみたいなものを簡単に作りたいと思います! UIKitよりSwiftUIの方がかなりシンプルにかけるので、その違いを意識してみるといいかもしれません笑
※勉強中のため、正しい説明になっていないかもしれません。予めご了承いただければと思います、、、

環境

  • macOS Catalina(10.15.2)
  • xcode(11.3)
  • Swift5.1
  • SwiftUI

こんな感じです

Listスクリーンショット 2020-01-01 20.18.31.png
ContentView.swift
import SwiftUI

struct Hyoka: Identifiable {
    var id = UUID()
    var name: String
}

struct ContentView: View {

    var body: some View {
        let hyokas = [Hyoka(name: "氷菓"),
                      Hyoka(name: "愚者のエンドロール"),
                      Hyoka(name: "クドリャフカの順番"),
                      Hyoka(name: "遠回りする雛"),
                      Hyoka(name: "ふたりの距離の概算"),
                      Hyoka(name: "いまさら翼といわれても")]

        return List(hyokas) { hyoka in
            Text(hyoka.name)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

解説

今回のポイントはHyoka構造体にIdentifiableプロトコルを実装していると言う点です。Identifiableプロトコルの中身は、

public protocol Identifiable {

    /// A type representing the stable identity of the entity associated with `self`.
    associatedtype ID : Hashable

    /// The stable identity of the entity associated with `self`.
    var id: Self.ID { get }
}

となっており、Hyoka構造体はidをプロパティとして定義する必要があります。idの中身はUUID構造体
を利用しており一意なInt型の値が入っています。(自分でidを入れても構わないです。)

このようにすればListができるのですが、なぜそもそも一意のidを付与しなければならないのか??についてもう少し説明を加えます。
その答えはずばり、SwiftUI側がそのViewで利用されているデータがどれなのかを特定しておかないと、データが更新されたときにどのViewを再構築すればいいのかわからなくなってしまうからです。
今回の場合(※プロパティラッパーを利用した変数定義をしていないのであまり適切ではありませんが)、Textビューを6個生成しています。仮に5番目(0から数えて)の
 Hyoka(name: "ふたりの距離の概算") => Hyoka(name: "神山高校") 
に変更したとしても、idを構造体側で予め持ってないとSwiftUIは5番目のTextビューまで特定することができません。だから、idをつける必要があるのです。

参考

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

Github ActionsでDanger+SwiftFormat

はじめに

まずは、あけましておめでとうございます。
去年は新卒でいろいろとヨイショしていただいたので今年はもっと自走できる用にしたいと思います。

本稿はiOSプロジェクトでDangerSwiftFormatによるプルリクの検査をGithub Actionsで行おうとしたときのメモです。
Github Actionsについては本稿では詳しく述べません(できない)のであしからず…

workflow file

とりあえず結論から。2020/1/2 時点で使用しているのは以下のworkflow fileです

※2020/1/2 修正: cacheを使うようにしました。参考

name: Danger

on: [pull_request]

jobs:
  build:
    runs-on: macos-latest

    steps:
    - uses: actions/checkout@v1

    - name: Setup ruby
      uses: actions/setup-ruby@v1
      with:
        ruby-version: '2.6.3'

    - name: Cache bundle
      uses: actions/cache@v1
      with:
        path: vendor/bundle
        key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-gem-

    - name: Bundle install
      run: |
        gem install bundler
        bundle config path vendor/bundle
        bundle install --jobs 4 --retry 3

    - name: Cache CocoaPods
      uses: actions/cache@v1
      with:
        path: Pods
        key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
        restore-keys: |
          ${{ runner.os }}-pods-

    - name: Pod install
      run: bundle exec pod install

    - name: Run danger
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: bundle exec danger

コードで大体わかると思いますが、以下のことを行っています。

  1. プルリク(関連のイベント)をトリガーに
  2. ブランチのチェックアウト
  3. Rubyのインストール
  4. Bundleのキャッシュ
  5. BundlerのインストールとGemfileのライブラリをインストール
  6. CococaPodsのキャッシュ
  7. Podfileのライブラリをインストール
  8. Dangerを実行

ライブラリ構成

SwiftFormat込みでこのworkflowを動かすには、

Bundler
  ├ Danger
  ├ danger-swiftformat
  └ CococaPods
      └ SwiftFormat/CLI

の構成でのライブラリの管理が最低限必要になります。

Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "cocoapods", "1.8.4"
gem "danger", "6.1.0"
gem "danger-swiftformat", "0.6.0"


Podfile
target 'HOGE' do
  pod 'SwiftFormat/CLI', :git => 'https://github.com/nicklockwood/SwiftFormat', :tag => '0.40.14', :configurations => ['Debug']
end


Dangerfile
swiftformat.binary_path = "Pods/SwiftFormat/CommandLineTool/swiftformat"
swiftformat.check_format(fail_on_error: true)

はまったところ

Github Actionsは元々dangerサポート?

Github ActionsでDangerやりたいなーと調べていたとき、

Github Actionsは元々dangerサポートしているので run: danger でおk

みたいな記事を参考にしてやってたのですが、no such commandでどっちやねんってなってました。
結局Bundlerでインストールしてbundle exec dangerでうまくいきました。

Invalid `Dangerfile` file: No such file or directory - swiftformat

これは単純にSwiftFormatのバイナリファイル(実行するファイル)を指定していなかったためでした。
swiftformat.binary_path = "Pods/SwiftFormat/CommandLineTool/swiftformat"で明示的に指定して解決しました。

Invalid `Dangerfile` file: Error running SwiftFormat: Error: Pods/SwiftFormat/CommandLineTool/swiftformat: 12: Pods/SwiftFormat/CommandLineTool/swiftformat: Syntax error: "(" unexpected

これは結構悩みました… エラー文言に惑わされて、パスの文字列に変な文字でも入ってるかな?と最初疑いましたが関係ありませんでした。
結論から言うと、実行環境をubuntu-latestからmacos-latestにしたことで解決しました。
ちゃんと検証していないので確定ではないですが… 僕のMacのローカル環境でのSwiftFormatのバイナリのパスはPods/SwiftFormat/CommandLineTool/swiftformatだったのですが、Ubuntuでは違うものになるのかなと推測しています。

最後に

Github Actions便利ですね!
はじめはBitriseで同じことをしようと思っていましたが、明らかにスマートに実装できています。

新年1日目から投稿できた!

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

【Swift】インスタンスの生成と利用の分離、Composition Rootという考えについて

はじめに

文章の中で「クラス」という言葉を用いていますが
これはSwiftのclassだけを指しているのではなく
structenumなども全て含めてクラスと呼称しています。


アプリを開発をしていると

  • 必要な機能をクラスに分けて開発する
  • さらにモジュールに分けて開発する
  • プロジェクトを超えて共通で利用できるモジュールを利用する

など
を行うことがあるかと思います。

そうすると
あるクラスの中で他のクラスを利用することがあるかと思いますが
どこで利用するクラスを生成するかが
問題になることがあります。

例えば
Aクラスの中で利用するBクラスのインスタンスを生成しているとします。

仮にBクラスのインスタンス生成時に
新しくCクラスを引数に渡す必要が出てきた場合
本来Aクラスでは必要のないCクラスのインスタンスの生成も行うことになります。

もしこの新しく追加したCクラスのインスタンス生成方法に変更があった場合
Aクラスには全く必要のないCクラスに依存していることによって
Aクラスが変更しなければならないという事態に陥ってしまいます。

この問題を解決する方法として
クラスの外部で必要なインスタンスの生成を行い
それを渡してあげるようにする
DI(依存性注入)を行う方法があります。

DIにはいくつかの種類が存在し

  • インスタンス生成時に引数として渡す(コンストラクタインジェクション)
  • プロパティを用意してインスタンス生成後に変数に値を設定する(プロパティインジェクション)
  • メソッドの引数として渡す(メソッドインジェクション)

があります。

この中でも
コンストラクタインジェクションを利用すると
クラス同士の依存関係を明確にできたり
値の設定忘れを防ぐことができるため
より好ましいと言われています。

DIを行うことで
クラス同士が密接に結合することを防ぎ(疎結合)
一つのクラスの変更の影響範囲を抑えることができますが
DIを行う上で一つ気をつけなければいけないことがあります。

それはDIをどこで行うかということです。

例えば
色々な場所でDIを行っていると
仮にインスタンスの生成方法に変更が入った際
複数ファイルに飛び散ったインスタンスの生成箇所を探す必要が出てくるかもしれません。

コンストラクタインジェクションの場合は
コンパイラが教えてくれるので良いかもしれませんが
プロパティインジェクションの場合は
どこかで設定漏れがあっても
気がつくタイミングが遅れてしまう可能性も考えられます。

こういったリスクを軽減する方法として

Composition Root

という考えを活用する方法があります。

今回は

  • インスタンスの生成と利用の分離
  • Composition Root

について見ていきたいと思います。

インスタンスの生成と利用を分離する

例えば
QiitaのAPIから投稿一覧を取得するアプリを考えてみます。

qiita.png

アプリは
NetworkとUIモジュールに分かれているとします。

そして
各クラスの役割は


QiitaLoader

一覧に表示する情報を外部ネットワークから取得するクラス

QiitaUserImageLoader

一覧情報に含まれたユーザ画像URLから画像データを外部ネットワークから取得するクラス

QiitaListViewModel

一覧に表示するデータや画面の状態を管理するクラス
QiitaLoaderで取得したデータをQiitaListViewControllerに通知する
(※ 今回はクロージャを経由して渡します。)

QiitaCellViewModel

一覧の各Cellに表示するデータや画面の状態を管理するクラス
QiitaUserImageLoaderで取得したデータをQiitaCellViewControllerに通知する

QiitaListViewController

QiitaListViewModelとUITableViewを繋げるUIViewController
QiitaCellViewControllerの配列を管理する

QiitaCellViewController

QiitaCellとQiitaCellViewModelを繋げるUIViewController

QiitaCell

各Qiitaの情報を表示するUITableViewCellのサブクラス


QiitaCellViewControllerは一覧情報を取得した後に
インスタンスを生成する必要があります。
そして
個々のQiita情報に対するユーザ画像を取得するために
一つ一つのQiitaCellViewController
QiitaUserImageLoaderのインスタンスを渡す必要があります。

そのため
QiitaListViewControllerでは

  • QiitaCellViewControllerの配列
  • QiitaLoader
  • QiitaUserImageLoader

への参照を保持する必要があります。

この時の
各インスタンスの生成場所
について考えてみます。

QiitaListViewControllerの内部で生成する

利用されるクラスの生成を
利用するクラスで生成する場合を考えます。

この場合
デメリットがいくつか想定されます。

  • 全く関係のない変更の影響を受ける可能性がある
  • テストがしづらい場合がある

全く関係のない変更の影響を受ける

例えば
それぞれのインスタンス生成時に
新しく引数が渡す必要が出てきた場合
QiitaListViewControllerでも引数を渡す変更を入れる必要があります。

final class QiitaLoader {
    let newParameter: String
    init(newParameter: String) {
        self.newParameter = newParameter
    }
}

final class QiitaListViewModel {
    let loader: QiitaLoader
    init(loader: QiitaLoader) {
        self.loader = loader
    }
}

final class QiitaListViewController: UIViewController {
    // こっちでも変更が必要になる
    let viewModel = QiitaListViewModel(loader: QiitaLoader(newParameter: "new parameter"))
}

QiitaListViewControllerでは
QiitaListViewModelのインスタンス生成時に
QiitaLoaderを渡しているだけですが
変更の影響を受けてしまいます。

上記はとてもシンプルな例ですが
QiitaLoaderの初期化処理が複雑になってくると
直接関係のないコードも増え
見通しもどんどん悪くなっていきます。

テストがしづらい場合がある

例えば
QiitaLoader
URLSession.sharedを使ってネットワーク通信をしているとします。

final class QiitaLoader {
    func load(from url: URL, completion: @escaping ([Qiita]) -> Void) {
        URLSession.shared.dataTask(url: url) {
            ...
        }
    }
}

そうすると
ViewModelViewControllerのテストをする際にも
実際にネットワーク通信をする必要が出てきます。

これはテストの実行速度を落とし
もし接続先のサーバがメンテナンスなどで止まっていたら
テストが失敗するという可能性もあります。

例えば
チームで開発をしていて
masterブランチへのマージには
テストに通過する必要があるなどの場合ですと
サーバが利用可能になるまで待たなければならなかったり
CIを再実行しなければならなくなります。


URLProtocolのサブクラスを作成して直接接続させない方法もありますが
これも結構手間がかかります。

また
CIツールに障害が発生したというケースは今回考えていません。

DIでオブジェクトの作成と利用を分離する

このような問題に対応する方法として
まずDIによってオブジェクトの生成を分離します。

これは外部から生成したオブジェクトを渡すようにします。

下記の例ではUIViewControllerなので
Storyboardを利用していることを想定して
プロパティインジェクションを利用しています。

※ iOS13で登場した引数を渡せるコンストラクタについては
今回は割愛させていただきます。

https://developer.apple.com/documentation/uikit/uistoryboard/3213988-instantiateinitialviewcontroller

final class QiitaListViewController: UIViewController {
    var viewModel: QiitaListViewModel!

}

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...

        let qiitaLoader = QiitaLoader(newParameter: "new parameter")
        let viewModel = QiitaListViewModel(loader: qiitaLoader)
        qiitaListViewController.viewModel = viewModel

        window?.rootViewController = qiitaListViewController
    }
}

これで
全く関係のないQiitaLoader変更の影響を受ける
という問題は解消できます。

抽象化でテストしやすくする

さらにもう一つのテストのしづらさの問題も考えてみます。
上記でも少し触れましたが
問題なのは直接ネットワークに接続しにいくことで
テストの速度や成否に影響を与えてしまうことでした。

そこでDIとして引数に渡すものを
抽象的なものにすることで
テスト時にモックなどへ入れ替えられるようにします。

protocol QiitaLoaderProtocol {
    func load(from url: URL, completion: @escaping ([Qiita]) -> Void)
}

final class QiitaListViewModel {
    let loader: QiitaLoaderProtocol
    init(loader: QiitaLoaderProtocol) {
        self.loader = loader
    }
}
final class QiitaLoaderStub: QiitaLoaderProtocol {
    func load(from url: URL, completion: @escaping ([Qiita]) -> Void) {
        completion([Qiita(title: "キータ!")])
    }
}

final class QiitaListViewModelTests: XCTestCase {
    func testLoadQiitaList() {
        let sut = QiitaListViewModel(loader: QiitaLoaderStub())
        ...
    }
}

こうすることで直接外部のネットワークで接続する必要がなくなり
テストの実行速度を高め
外部の事情でテストが失敗するということも少なくなります。

抽象化を行うということは
テストに限らず
今後変更が起こりうる可能性が箇所に導入することで
変更の影響範囲を抑えることができ
変更に強い設計にすることにも繋がります。

Composition Root

上記でいくつかの問題が解決されましたが
まだ似たような問題があります。

QiitaCellViewControllerのインスタンスの生成に関してです。
現在の設計ですと
QiitaCellViewControllerQiitaListViewControllerの内部で行われ
QiitaUserImageLoaderのインスタンスを渡すために
参照を保持しています。

final class QiitaListViewModel {
    let loader: QiitaLoaderProtocol
    init(loader: QiitaLoaderProtocol) {
        self.loader = loader
    }

    var onLoad: (([Qiita]) -> Void)?
    func load(from url: URL) {
        loader.load(from: url) { list in
            onLoad?(list)
        }
    }
}


final class QiitaCellViewModel {
    let qiita: Qiita 
    let loader: QiitaUserImageLoaderProtocol
    init(qiita: Qiita, loader: QiitaUserImageLoaderProtocol) {
        self.qiita = qiita
        self.loader = loader
    }

    var onLoad: (([Qiita]) -> Void)?
    func load(from url: URL) {
        loader.load(from: url) { list in
            onLoad?(list)
        }
    }
}

final class QiitaListViewController: UIViewController {
    var viewModel: QiitaListViewModel!
    var imageLoader: QiitaUserImageLoaderProtocol!
    var cellViewControllers: [QiitaCellViewController] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.onLoad = { [weak self] qiitaList in
            qiitaList.forEach { qiita in
                guard let self = self else { return }
                let cellViewController = QiitaCellViewController.fromStoryboard()
                cellViewController.viewModel = QiitaCellViewModel(
                                                   qiita: qiita, loader: self.imageLoader)
                self.cellViewControllers.append(cellController)        
            }
        }
    }

    func getList(from url: URL) {
        viewModel.load(from: url)
    }
}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...

        let qiitaLoader = QiitaLoader(newParameter: "new parameter")
        let viewModel = QiitaListViewModel(loader: qiitaLoader)
        qiitaListViewController.viewModel = viewModel
        qiitaListViewController.imageLoader = QiitaUserImageLoader()

        window?.rootViewController = qiitaListViewController
    }
}

QiitaCellViewControllerを生成するためには
一覧情報を取得する必要があるため
QiitaListViewControllerと引き離すことができません。

しかし
QiitaLoaderの時と同様に
QiitaUserImageLoaderQiitaCellViewController
変更からの影響を受けてしまいます。

そんな時に活用できる
Composition Rootという考えについて
次に見ていきたいと思います。

Composition Rootとは?

モジュール同士を繋げるアプリ上のできる限り単一の場所


モジュール同士となっていますが
クラス同士でもDIが必要な場合は同じであると思っています。


「できる限り」とされているのは
決して一つのクラスにしなければいけないという訳ではなく
同じモジュール内にある限り
複数のクラスでもメソッドでも問題はないからだそうです。

と定義されています。

つまり
DIする(インスタンスを生成する)場所を集約しよう
ということです。

こうすることで

  • インスタンスの管理がしやすくなる
  • クラスが疎結合になる(不要な変更の影響を受けなくなる)
  • 各クラス同士の関係が一箇所でわかる
  • 変更箇所を集約できる
  • プロパティインジェクションの設定忘れのリスクを減らせる

などのメリットが挙げられます。

Composition Rootにする場所

さらに
Composition Rootとする場所としては

できる限りアプリのエントリーポイントに近いところが良い

とされています。

この理由としては
DIをエントリーポイントに近いところで行うと
アプリの内部は具体的な実装について知る必要が少なくなり
より変更に強い設計になると考えられています。
(いわゆる実装の判断を遅らせるということです。)

iOSアプリだと
AppDelegateSceneDelegateがある
メインモジュールにあるのが
好ましいのではないかと思います。

Composition Rootを取り入れる

それでは
先ほどの例でComposition Rootを使用します。

qiita2.png

struct QiitaListUIComposer {
    static func composeQiitaListViewController(qiitaLoader: QiitaLoaderProtocol, imageLoader: QiitaImageLoaderProtocol) -> QiitaListViewController {
        let viewController = QiitaListViewController.fromStroyboard()
        let viewModel = QiitaListViewModel(loader: qiitaLoader)
        viewModel.onLoad = { [weak viewController] items in
            let cellViewControllers: [QiitaCellViewController] = qiitas.map { qiita in
                QiitaCellViewController(
                    viewModel: QiitaListCellViewModel(
                        qiita: qiita,
                        loader: imageLoader)
            }
            viewController?.cellViewControllers.append(contentsOf: cellViewControllers)
            viewController?.updateTableView()
        }
        viewController.viewModel = viewModel

        return viewController
    }
}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        ...
        let viewController = QiitaListUIComposer.composeQiitaListViewController(
                                          qiitaLoader: QiitaLoader(newParameter: "new parameter"), 
                                          imageLoader: QiitaUserImageLoader())
        window?.rootViewController = viewController
    }
}

こうすることでQiitaListViewControllerから
QiitaUserImageLoaderの変更の影響を受けることがなくなりました。

さらに
今回登場したクラスのインスタンス生成に将来的に変更が入る場合でも
このQiitaListUIComposerに全てがまとまっているため
どこを修正すればよいかに悩む要素も減ります。

Adapterパターン

上記のComposerの中でもう一つ注目したい点として
ComposerはAdapterパターンを適応しています。

それがviewModel.onLoadの部分です。

QiitaListViewModelから[Qiita]を受け取りますが
QiitaListViewControllerでは[QiitaCellViewController]が必要になります。

それをQiitaListUIComposerが担うことで
仮にQiitaListViewModelから受け取る値に変更があったとしても
QiitaListViewControllerはただ必要な値を受け取るだけで
変更を加える必要がなくなります。

https://ja.wikipedia.org/wiki/Adapter_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

今回は割愛しますが
ここはかなり複雑になっているのでメソッドや別のクラスとして
切り出した方が良いかもしれません。

Composition Rootをもっと活用する

Composition Rootを活用することで
インスタンスの管理しやすさや変更の影響を少なくするなどの
メリットを見てきましたが
その他でも有効活用できる方法を見ていきます。

Decoratorパターン

QiitaLoaderやQiitaUserImageLoaderが
外部からデータを取得する時に
バックグラウンドスレッドで処理を実行する場合があります。
URLSessionもバックグラウンドで実行します。

一方で
UIKitには
メインスレッドで処理を行わなければいけないという制約があります。

Important: Use UIKit classes only from your app’s main thread or main dispatch queue, 
unless otherwise indicated. 
This restriction particularly applies to classes 
derived from UIResponder or that involve manipulating your app’s user interface in any way.

この時にメインスレッドで処理行う場合に
DispatchQueueを利用します。

例えば
QiitaListViewControllerでUITableViewを更新する時に

final class QiitaListViewController: UIViewController {
    func updateTableView() {
        DispatchQueue.main.async { [weak self] in
            self?.tableView.reloadTableView()
        }
    }
}

しかし
こうしてしまうと
QiitaLoaderを利用するクラス内で
繰り返し同じコードを記載する必要が出てきますし
メインスレッドに戻して処理をするというのは
Loaderの内部的な事情に影響を受けていることになり
結合が不用意に強くなってしまっています。

例えばLoaderがメモリからデータを取得する場合などは
メインスレッドに戻す処理が必要ないかもしれません。

この時にDecoratorパターンを活用することで
この問題を解消できる場合があります。

https://ja.wikipedia.org/wiki/Decorator_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

Decoratorパターンは
ざっくり言うと既にある機能の上にさらに機能を動的に追加するものです。

Swiftではprotocolを使ってこれを簡単に実現できます。

final class DispatchMainQueueDecorator<T> {
    private let decoratee: T

    init(_ decoratee: T) {
        self.decoratee = decoratee
    }

    func dispatch(completion: @escaping () -> Void) {
        guard Thread.isMainThread else {
            return DispatchQueue.main.async(execute: completion)
        }
        completion()
    }
}

extension DispatchMainQueueDecorator: QiitaLoaderProtocol where T == QiitaLoaderProtocol {
    func load(from url: URL, completion: @escaping ([Qiita]) -> Void) {
        decoratee.load(from: url) { [weak self] result in
            self?.dispatch { completion(result) }
        }
    }
}

struct QiitaListUIComposer {
    static func composeQiitaListViewController(qiitaLoader: QiitaLoaderProtocol, imageLoader: QiitaImageLoaderProtocol) -> QiitaListViewController {
        ...
        let viewModel = QiitaListViewModel(loader: DispatchMainQueueDecorator(qiitaLoader))
        ...
    }
}

こうすることで
UIモジュールの方では
メインスレッドで処理をする必要があるかどうかを気にする必要がなくなり
重複してDispatchQueue.main.asyncというコードを記載する必要もなくなります。

Proxyパターン

似たような形ですが別のパターンを適用して
別の問題を解決してみます。

例えば
QiitaListViewModelでデータを取得する前後で
通信の開始と終了をDelegateパターンで伝達する機能を追加します。


現実的ではないかもしれませんがご了承ください。

protocol QiitaListViewModelDelegate: AnyObject {
    func startLoading()
    func endLoading()
}

final class QiitaListViewModel {
    let loader: QiitaLoaderProtocol
    let delegate: QiitaListViewModelDelegate
    init(loader: QiitaLoaderProtocol, delegate: QiitaListViewModelDelegate) {
        self.loader = loader
        self.delegate = delegate
    }

    var onLoad: (([Qiita]) -> Void)?
    func load(from url: URL) {
        delegate.startLoading()
        loader.load(from: url) { list in
            onLoad?(list)
            delegate.endLoading()
        }
    }
}

ここでQiitaListViewController
このDelegateを実装して
通信中にインディケーターを表示するようにします。

final class QiitaListViewController: UIViewController {
    @IBOutlet private(set) weak var indicator: UIActivityIndicatorView!

    private let viewModel: QiitaListViewModel
}


extension QiitaListViewController: QiitaListViewModelDelegate {
    func startLoading() {
        indicator.startAnimating()
    }

    func endLoading() {
        indicator.stopAnimating()
    }
}

struct QiitaListUIComposer {
    static func composeQiitaListViewController(qiitaLoader: QiitaLoaderProtocol,
                                               imageLoader: QiitaImageLoaderProtocol)
        -> QiitaListViewController {
            ...
            let viewController = QiitaListViewController.fromStoryboard()
            let viewModel = QiitaListViewModel(loader: DispatchMainQueueDecorator(qiitaLoader),
                                               delegate: viewController)
            viewController.viewModel = viewModel
            ...
    }
}

ここで問題が発生します。

QiitaListViewControllerQiitaListViewModelを強参照していて
QiitaListViewModeldelegateを経由してQiitaListViewControllerを強参照しています。
循環参照が起きました。

これを解消するためにはdelegateweakを設定して弱参照にします。

final class QiitaListViewModel {
    weak var delegate: QiitaListViewModelDelegate?
    init(loader: QiitaLoaderProtocol, delegate: QiitaListViewModelDelegate) {
        self.loader = loader
        self.delegate = delegate
    }

    var onLoad: (([Qiita]) -> Void)?
    func load(from url: URL) {
        delegate?.startLoading()
        loader.load(from: url) { list in
            onLoad?(list)
            delegate?.endLoading()
        }
    }
}

しかし
これはiOSのメモリ管理方法の事情であり
Presenterとは関係がありません。

PresenterをiOS以外で利用する場合に
もしかしたらweakにする必要がないかもしれません。

さらにこのweakが色々なところに指定されていると
より大規模な開発では
見えないところで問題を発生させることもあるかもしれません。

そこで
Proxyパターンを活用することを検討してみます。

https://ja.wikipedia.org/wiki/Proxy_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

Proxyパターンは
使用したいオブジェクトの入れ物を用意して
外からは中のオブジェクトにアクセスするのと同様に
アクセスするようにする方法です。

Swiftではstandard libraryの
AnySequenceなどのTypeEraserの実装で利用されています。

private final class WeakReferenceProxy<T: AnyObject> { |
    private weak var object: T?
    init(_ object: T) {
        self.object = object
    }
}

extension WeakReferenceProxy: QiitaListViewModelDelegate where T: QiitaListViewModelDelegate {
    func startLoading() {
        object?.startLoading()
    }

    func endLoading() {
        object?.endLoading()
    }
}

struct QiitaListUIComposer {
    static func composeQiitaListViewController(qiitaLoader: QiitaLoaderProtocol, imageLoader: QiitaImageLoaderProtocol) -> QiitaListViewController {
        ...
        let viewController = QiitaListViewController.fromStoryboard()
        let viewModel = QiitaListViewModel(loader: DispatchMainQueueDecorator(qiitaLoader))
        viewModel.delegate = WeakReferenceProxy(viewController)
        viewController.viewModel = viewModel
        ...
    }
}

上記のように
WeakReferenceProxy
weakの実装を隠しています。

こうすることで

  • メモリ管理という内部事情を外に漏らさなくなる
  • weakで参照する必要があるインスタンスの管理を集約できる

などができるようになります。

まとめ

DIという方法を通して
インスタンスの生成と使用を分離することで変更の影響を抑えるメリットや
Composition Rootという考えを利用して
クラス依存関係を簡潔に管理する方法を見てきました。

開発の規模が大きくなればなるほど
クラス間の依存関係は複雑になり
余計なインスタンスの生成をしてメモリの使用量が増えたり
違うインスタンスを参照してしまった結果
予期せぬ動きをしてしまうことがあるかもしれません。

開発規模によっては
ここまで大きな仕組みは必要ないかもしれませんが
Composition Rootという考えを知っておくことで
余計な複雑性を持ち込むリスクを減らすことは
できるのではないかと思います?

もし間違いなどございましたらご指摘していただけると嬉しいです??‍♂️

参考資料

https://freecontent.manning.com/dependency-injection-in-net-2nd-edition-understanding-the-composition-root/
https://blog.ploeh.dk/2011/07/28/CompositionRoot/

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

Swift初心者が年末年始を利用してSwiftUI Tutorialsをやってみた

はじめに

今更感はありますが、年末年始で時間ができたので以前からずっと気になっていたSwiftUIを触ってみようと、Appleから提供されているTutorialsをやってみました。(英語能力があれなのでGoogle先生にお世話になりながら...)
学生時代に研究でObjective-Cで一般未公開ながらiPad向けアプリを作成したことがありましたが、Swiftはほとんど初心者だったため、Tutorialをやっているだけでも「Swiftってこんな書き方するんだ...」みたいな発見が多々ありました。
せっかくなのでSwift・SwiftUI含め詰まったことを自分用の備忘録として雑多にメモメモ。

やったやつ

Creating and Combining Views
Building Lists and Navigation
Handling User Input

structとclass

struct ContentView: View {
    var body: some View {
        Text("Turtle Rock")
            .font(.title)
            .foregroundColor(.green)
    }
}

structという文字から最初はC言語の構造体みたいな物なのかなと思いきや、関数も宣言できる。
他の言語同様classもあり、structclassの違いとしては以下の通り。

  • structは継承ができない
  • classは参照型、structは値型

PreviewProvider

Xcodeで作成中のViewをプレビューする内容を設定するやつ(雑。
File > New > File...Swift UIViewを選択してファイルを作成すると自動で作成されるstruct
previewsの中でViewの表示方法を定義できる。

// 普通にContentViewを表示
struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ソースを直すとプレビューに即座に反映される。
プレビューからも部品のプロパティを弄れるし、ソースコード側に反映される。
まぁ、 EclipseとかIDEによっては同様な機能を持っている物はある。
面白いと思ったのはPreviewProviderでの定義方法によって表示をある程度自在にできるところ。
例えば、複数の部品をグループ化して表示する。しかもサイズを設定する。

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

例えば、端末別に表示をする等ができる。

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max", "iPad Pro (12.9-inch)"], id: \.self) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
    }
}

VStackとHStack

簡単にレイアウトできる。HTMLより楽。

  • VStack{} - 垂直方向にコンポーネントを並べる
  • HStack{} - 水平方向にコンポーネントを並べる

Stackは入れ子にもできる。
また、コンポーネント間の空白や余白も用意されている。

  • Spacer() - 空白作る
  • Padding() - 余白作る
  • Offset() - 位置をずらす
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)
                HStack(alignment: .top) {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)
                }
            }

アプリ立ち上げ時の起動画面

アプリ立ち上げ時の起動画面(RootView)はSceneDelegate.swiftscene関数で設定する。
window.rootViewControllerにRootViewに設定したいViewをセットする。

SceneDelegate.swift
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            // UIHostingController()のrootViewに作成したViewを設定する。
            window.rootViewController 
                = UIHostingController(rootView: LandmarkList())
            self.window = window
            window.makeKeyAndVisible()
        }
    }

実際にアプリを実装する時、起動時に必要な初期処理(ファイル読み込みとか?)などはどこで書くのがベターなのかな?
画面と処理は分けた方が良さそうなので、RootViewに設定したViewの中で初期処理をやるのは何か違う気がする。
チュートリアルを進めたら出てくるのかな。

protocol

Javaとかでいうinterface的なアレ。
構造体でも使える。

Landmark.swift
// HashableとCodableとIdentifiableを実装している。
struct Landmark: Hashable, Codable, Identifiable {
// 略
}

ObservableObject

データを監視するためのオブジェクトを作成する。
ObservableObjectプロトコルを実装するclassを作成する。

変更を他クラスから参照できるよう、公開するプロパティには@Published属性を付ける。

UserData.swift
import SwiftUI
import Combine

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var landmarks = landmarkData
}

参照する側ではObservableObjectを@EnvironmentObject属性を付けて宣言する。

LandmarkList.swift
struct LandmarkList: View {
    // 監視するやーつ
    @EnvironmentObject var userData: UserData

    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Favorites only")
                }

                ForEach(userData.landmarks) { landmark in
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}

場所によって参照先の記述方法が変わっているのはなんじゃろ?

  • $userData.showFavoritesOnly - ドル記号はバインディングしていることを表している模様。値の変更を監視している。
  • ForEach(userData.landmarks) { landmark in - 下記でselfを付けてこちらにselfを付けない理由はなんだろ?
  • !self.userData.showFavoritesOnly - selfはJavaとかでいうthisのイメージ。

続く

Tutorialをやっただけですが、面白かったので多分続きます。

参考

SwiftUIチュートリアルをやってみた その1
Swift では Protocol を積極的に使おう

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

swiftでwebAPIを呼び出してjsonデータを表示させる

概要

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

 前回のJSON形式でレスポンスをするwebAPIを作成するで(モックアップのレベルですが)リクエストパラメータで指定した番号のクーポン情報をjson形式でレスポンスするクーポン配信APIができたので、リクエストする側のアプリをSwiftで実装します。

今回は

  1. アプリからwebAPIにリクエストを投げる
  2. レスポンスされた情報をjson形式でコンソールに表示する

の流れを一旦完成させ、アプリ画面にクーポンを表示する下準備をしようと思います。

参考

環境

Mac OS 10.15
Swift5
Xcode11.1

アプリの仕様

  • アプリ起動時にAPIにリクエストし、レスポンスされたクーポン情報をjson形式でコンソールに表示する。
  • webAPIにリクエストする際、リクエストパラメータとしてクーポンコードを指定する。

手順

  • Xcodeでクライアントアプリ側のプロジェクトを作成
  • http通信を許可する設定をする
  • webAPIとの通信部分をswiftで実装
  • webAPIから受け取った情報を画面に表示する処理を実装
  • 動作確認

Xcodeでクライアントアプリ側のプロジェクトを作成

現在、SwiftUIがリリースされていますが、今回は多少経験のある従来のstoryboadを使います。

Xcodeを開いて、「Create a new Xcode project」を選択。
open-xcode-mark.png

次の画面で、iOS の Single View App を選択。
select-project-type-mark.png

次の画面でプロジェクトのオプションを設定。
この時、User Interface: は SwiftUI ではなく Storyboardを選択します。Product Name: や Team: Organization Name: などはご自身の設定を入れてください。

choose-options-of-project-mask.png

http通信を許可する設定をする

参考にさせて頂いたサイトで紹介されている手順に従い、Info.plist ファイルを編集して http通信を許可します。

webAPIとの通信部分をswiftで実装

とりあえず、swift側で 前回までに作成したクーポン配信APIを呼び出して、レスポンスを得られるところまで作成します。

swiftのViewController.swiftを下記の通り実装します。
Apiから取得する data、response、error の中身をコンソールに出力する事でswift側でapiのレスポンスを得られたか判断します。APIへのリクエストURLはコードにベタがきします。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // アプリがロードされた時に実行させたい処理を書きます

        let url: URL = URL(string: "http://127.0.0.1:8000/coupon/?coupon_code=0001")!
        let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
            // コンソールに出力           
            print("data: \(String(describing: data))")
            print("response: \(String(describing: response))")
            print("error: \(String(describing: error))")
        })
        task.resume()        
    }
}

djangoのサーバを起動してクーポン配信APIを立ち上げたら、Xcodeでプログラムを実行してみます。iPhoneのシミュレータが起動しアプリの画面が開いたらXcodeのコンソール出力を確認します。画面には何も表示設定をしていないのでただの白い画面です。

Xcodeのコンソール出力を見るとdata、response、error の内容が出力されているので、レスポンスを得られていることを確認出来ました。
swift-testrun-api-call-001-mask.png

django のサーバを起動したターミナルを確認すると、GETでリクエストを受けていることを確認できます。
swift-testrun-api-response-001-mask.png

レスポンスで受け取ったdataをswift側でJSONに変換する

APIとの疎通まで確認出来ましたので、次はレスポンスされたデータから情報を取り出すために、data型をJSONに変換する処理を追加します。

URLSessionTaskのクロージャー部分にJSONに変換するためのコードを追加します。クロージャーに関する解説は参考にさせて頂いた記事をご覧ください。

ViewController.swift のURLSessionTaskの部分に、print([json形式の変数])を入れて、JSONに変換出来たか確認します。

ViewController.swift
       do{
                let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
                print(coupon_data) // Jsonの中身を表示
            }
            catch {
                print(error)
            }

djangoのサーバが起動中なのを確認してswiftを実行し、コンソール出力を見ます。コンソール出力にjsonの中身が表示されているので変換は成功です。

swift-testrun-api-call-002.png

jsonの中身は見れましたが、coupon_benefitsの部分が よく分からない文字列("1000\U5186\U5f15\U304d\U30af\U30fc\U30dd\U30f3\Uff01”)になっています。
これを本来の文字列にするには、jsonのパースが必要です。

jsonをパースする

data型をjsonに変換しただけだとANY型になっており、上記のようによく分からない文字列で表示されてしまいます。なので、ANY型をString型にキャストする必要があります。

let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)

の後ろに、as! [String: Any] を付けます。

let couponData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [String: Any]

djangoのサーバが起動中なのを確認してアプリを実行し、コンソール出力を見るます。
"coupon_benefits": 1000円引きクーポン!になっているのでパース成功です。
swift-testrun-api-call-003.png

下記のコードは不要になったので削除します。

print("data: \(String(describing: data))")
print("response: \(String(describing: response))")
print("error: \(String(describing: error))")

以上でアプリの画面にapiで取得した情報を表示する準備が出来ました。

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

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

16頁 第1章 ボリューム特典ポイントの計算部分の抽出 volumeCreditsFor関数内変数名変更

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 statement(invoice:Invoice, plays:Dictionary<String, Play>) -> String {
    var volumeCredits = 0
    var totalAmount = 0
    var result = "Statement for \(invoice.customer)\n"
    let format = NumberFormatter()
    format.numberStyle = .currency
    format.locale = Locale(identifier: "en_US")

    for perf in invoice.performances {
        volumeCredits += volumeCreditsFor(aPerformance: perf)

        result += "  \(playFor(aPerformance: perf).name): " + format.string(from: NSNumber(value: amountFor(aPerformance: perf) / 100))! + " (\(perf.audience) seats)\n"
        totalAmount += amountFor(aPerformance: perf)
    }
    result += "Amount owed is " + format.string(from: NSNumber(value: totalAmount / 100))! + "\n"
    result += "You earned \(volumeCredits) 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でコーディング その10

15頁 第1章 ボリューム特典ポイントの計算部分の抽出

Swift版 main.swift

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

import Foundation

makeData()

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

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

func statement(invoice:Invoice, plays:Dictionary<String, Play>) -> String {
    var volumeCredits = 0
    var totalAmount = 0
    var result = "Statement for \(invoice.customer)\n"
    let format = NumberFormatter()
    format.numberStyle = .currency
    format.locale = Locale(identifier: "en_US")

    for perf in invoice.performances {
        volumeCredits += volumeCreditsFor(perf: perf)

        result += "  \(playFor(aPerformance: perf).name): " + format.string(from: NSNumber(value: amountFor(aPerformance: perf) / 100))! + " (\(perf.audience) seats)\n"
        totalAmount += amountFor(aPerformance: perf)
    }
    result += "Amount owed is " + format.string(from: NSNumber(value: totalAmount / 100))! + "\n"
    result += "You earned \(volumeCredits) 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で続きを読む