20191203のSwiftに関する記事は18件です。

【Swift】UI自動化テストフレームワークEarlGreyを使ってみた

この記事は、TECOTEC Advent Calendar 2019 の4日目の記事です。

開発環境:Xcode:10.2.1
開発言語:swift4.0

UI自動化テストフレームワークEarlGreyとは

Googleが作ったネイティブiOS用のUI自動化テストフレームワークです。
Google公式のGitHub
XCTestを拡張してありXcodeまたはコマンドラインにて実行が可能。

EarlGreyを選んだ理由

Androidと一緒に開発することが多いので、
Espressoと(たぶん)対になってるEarlGreyを使ってみようと思った。
あとはただの好奇心!

いざ導入!

しようと思ったら、まず記事が少ない!
2016年とかの記事ばっかりでそもそも情報少なくて心が折れかけながら
公式のInstall and runを見ながらとりあえず入れてみる。

そして案の定詰まった。。。

【原因】
流し読みしてたせいでstep2のAdd EarlGrey as a framework dependencyという部分を、実行し損ね2時間ぐらい無駄にしながらなんとか無事に導入完了。

UITest書いてみる

Googleが用意してくれたチートシートとにらめっこしながらそれっぽいものを探して、
ボタンを押して表示されるテキストが正しいかチェックするテスト書いてみた。

sample.swift
import XCTest
import EarlGrey

@testable import test

class testTests: XCTestCase {

    func testExample() {
        // ボタンをタップする
        EarlGrey().selectElementWithMatcher(grey_accessibilityID("testButton")).performAction(grey_tap())
        // TESTというテキストが表示されていることを確認する
        EarlGrey().selectElementWithMatcher(grey_text("TEST")).assertWithMatcher(grey_sufficientlyVisible())
    }
}    

とりあえず問題なく動いた。

2、3日遊んでみての感

・EarlGreyはシステムアラートのテストができない。(結構致命的)
・別にandroidに合わせてフレームワーク使う意味もない気がしてきた
・XcodeのXCUITestの方が記事がたくさん出てくる(当たり前)
・実はEarlGrey2がある。。。

【ちょっとだけEarlGrey2について】
EarlGreyの時にはテストできなかったアラートなどもテストできる用になっているので、今から触るなら断然EarlGrey2がオススメしたいが・・・
まだベータ版なので、試しに使うのはいいが本番稼働に使うのは微妙。
結構Issuesもあるし、毎日のようにコミットが走っているのでなんとも言えない。。。
そして案の定記事はほぼないのでそこがネック。
ただgoogleなので公式サンプルはそこそこある(全てが最新とは言わない)。

個人の感想ですが、XCUITestでいいかなーと思いました。

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

Swiftで型を実行時に作る

この記事は 型を実行時に作る:怖くないリフレクション(サンプルコードはHaskell)をSwiftに移植したものです。

型レベル自然数

剰余計算をする場合など、自然数に依存したデータ型を定義したいことがあります。この辺の動機付けは @taketo1024 さんの記事や SwiftyMath

を参照してください。

例えば、 Swift 以下のような感じで自然数を表す型(プロトコル)を定義できます。

protocol Nat {
    static var intValue: Int { get }
}

struct Zero : Nat { static let intValue = 0 }
struct Succ<T: Nat> : Nat {
    static var intValue: Int {
        T.intValue + 1
    }
}

typealias _0 = Zero
typealias _1 = Succ<_0>
typealias _2 = Succ<_1>
typealias _3 = Succ<_2>
typealias _4 = Succ<_3>
typealias _5 = Succ<_4>

プロトコル Nat に適合する型 T からは T.intValue という感じで整数値を取得できます。これを使って「mod m で計算する型」は、例えば次のように定義できます。

struct IntMod<m: Nat> {
    var x: Int
    init(_ x: Int) {
        self.x = x % m.intValue
    }
    static func + (_ lhs: Self, _ rhs: Self) -> Self {
        Self((lhs.x + rhs.x) % m.intValue)
    }
}

print(IntMod<_3>(5) + IntMod<_3>(2)) // => IntMod<Succ<Succ<Succ<Zero>>>>(x: 1)

いろいろ手抜きですが、そこは気にしないでください。(ちゃんとやるなら多倍長整数を使いたいところですが、ここでは簡略化のため Int を使っています)

実行時の値に基づいた型を作る

さて、普通は型と言ったらコンパイル時に決まっているものですが、実行時の値に依存した型を作りたい時があります。

例えば、筆者の「週刊 代数的実数を作る」で紹介した整数係数多項式の因数分解アルゴリズムでは、入力となる多項式に応じて剰余計算の法を選びます。

実行時の値に依存した型を作るというのは、どのプログラミング言語でもできることではありません。例えばC++やRustではそういうのは不可能でしょう。一方で、Haskellでは「型を実行時に作る:怖くないリフレクション」に書いたように、実行時の値を反映した型を構築することが可能です。

Haskellの場合でポイントとなっていたのは多相再帰でした。ではSwiftはどうなのかというと、多相再帰ができます。

例えば、次のような関数を考えます。この関数は Nat に適合する型 p に依存します。

func doSomeCalculation<p: Nat>(_: p) {
    print("doSomeCalculation \(p.intValue)")
    print(IntMod<p>(3) + IntMod<p>(2))
}

この関数を、実行時の値に基づいて呼び出してみましょう。つまり、次のような関数を定義します。

func doSomeCalculationWithDynamicVal(_ m: Int) {
    /*
       どうにかして
           doSomeCalculation(Succ<...Succ<Zero>...>())(Succがm回)
       みたいなことをしたい
    */
}

勿体ぶっても仕方がないのでコードを貼ってしまいますが、次のように書くと doSomeCalculation を値レベルの引数に基づいて呼び出せます:

func doSomeCalculationWithDynamicValRec<n: Nat>(_ acc: n, _ m: Int) {
    if m == 0 {
        doSomeCalculation(acc)
    } else {
        doSomeCalculationWithDynamicValRec(Succ<n>(), m-1)
    }
}

func doSomeCalculationWithDynamicVal(_ m: Int) {
    doSomeCalculationWithDynamicValRec(Zero(), m)
}

補助関数 doSomeCalculationWithDynamicValRec で多相再帰を行っています。与えられた型 n に対して、 Succ<n> によって自分自身を呼び出しています。C++やRustでこういうことをするとコンパイラーに怒られますが、Swiftでは怒られません。

この関数は、例えば次のように呼び出せます:

doSomeCalculationWithDynamicVal(3)
let x = Calendar(identifier: .iso8601).component(.minute, from: Date())
doSomeCalculationWithDynamicVal(x)

コンパイル時に決定しているリテラルだけではなく、実行時の日時のような動的な値に依存した計算もできることがわかります。

汎用的にする

型レベル自然数を使う処理ごとにさっきのような多相再帰を書くのは面倒です。

Haskellの場合はランク2多相があったので reifyNat :: Integer -> (forall n. (IsNat n) => Proxy n -> a) -> a みたいな関数を定義できましたが、Swiftにはランク2多相はなさそうです(あったら教えてください)。

仕方ないので、多相なメソッドを持つプロトコルを定義して、それを呼び出す関数という形で多相再帰の部分を汎用的にします。

protocol NatDependentAction {
    associatedtype Result
    func invoke<n: Nat>(_: n) -> Result
}

func reifyNatRec<n: Nat, F: NatDependentAction>(_ acc: n, _ m: Int, _ f: F) -> F.Result {
    if m == 0 {
        return f.invoke(acc)
    } else {
        return reifyNatRec(Succ<n>(), m-1, f)
    }
}

func reifyNat<F: NatDependentAction>(_ m: Int, _ f: F) -> F.Result {
    reifyNatRec(Zero(), m, f)
}

この NatDependentActionreifyNat は、例えば次のような感じで使えます。

struct SomeCalc: NatDependentAction {
    typealias Result = Void
    func invoke<n: Nat>(_: n) -> Void {
        print("SomeCalc \(n.intValue)")
        if n.intValue != 0 {
            print(IntMod<n>(3) + IntMod<n>(2))
        }
    }
}

reifyNat(7, SomeCalc())

C++の関数オブジェクトやJavaのSingle Abstract Methodを彷彿とさせるやり方になりました。

効率化する

現在の実装では自然数の値に応じた Succ 型を使うため、例えば、法が 1000000007 だと 1000000007 回多相再帰して Succ を 1000000007 回適用することになってしまいます。これは大変非効率的です。スタックオーバーフロー待ったなしです。

実は、自然数の表し方を変えることで、再帰の深度を O(n) から O(log n) に落とすことができます。

struct Double<T: Nat> : Nat {
    static var intValue: Int {
        2 * T.intValue
    }
}
struct DoublePlus1<T: Nat> : Nat {
    static var intValue: Int {
        2 * T.intValue + 1
    }
}

func reifyNat2Rec<n: Nat, F: NatDependentAction>(_ acc: n, _ b: Int, _ m: Int, _ f: F) -> F.Result {
    if b < 0 {
        return f.invoke(acc)
    } else if (m & (1 << b) == 0) {
        return reifyNat2Rec(Double<n>(), b-1, m, f)
    } else {
        return reifyNat2Rec(DoublePlus1<n>(), b-1, m, f)
    }
}

func reifyNat2<F: NatDependentAction>(_ m: Int, _ f: F) -> F.Result {
    reifyNat2Rec(Zero(), Int.bitWidth - m.leadingZeroBitCount - 1, m, f)
}

何をしたかというと、自然数の2進展開を使いました。この方法なら reifyNat2(1000000007, SomeCalc()) も楽々表現できます。

任意の値を表す

自然数以外の値にもこの方法を適用できると便利です。

まず、自然数の有限列は1個の自然数にエンコードできるので、この方法を使えそうです。(具体的な方法はここでは割愛します。数理論理学を勉強された方には、ゲーデル数とかでおなじみのやつです)

そして、いわゆるシリアライズという操作は、オブジェクトをバイト列で表すということをやっています。バイト列というのは自然数の列なので、シリアライズ可能なオブジェクトならこの方法を適用できることになります。

シリアライズが出来なさそうなオブジェクトの場合でも、希望はあります。コンピューターの64ビットのアドレス空間に乗ったオブジェクトであれば、64ビット整数で表せるのではないでしょうか?……そう、ポインターです。ポインターを整数値として受け渡してやれば、任意の値を型レベル自然数を通して受け渡しできそうです。

残念ながら筆者はSwiftには明るくないので、この辺は読者への課題とします。

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

Swift Network.framework Study 20191203

Study

Network.framework
Study:Client側

環境

Client:Swift、Xcode
Server:Java、NetBeans

Client Source Swift

import Foundation
import Network

func startConnection() {
    let myQueue = DispatchQueue(label: "ExampleNetwork")
    let connection = NWConnection(host: "localhost", port: 7777, using: NWParameters.tcp)
    connection.stateUpdateHandler = { (newState) in
        switch(newState) {
        case .ready:
            print("ready")
            sendMessage(connection)
        case .waiting(let error):
            print("waiting")
            print(error)
        case .failed(let error):
            print("failed")
            print(error)
        default:
            print("defaults")
            break
        }
    }
    connection.start(queue: myQueue)
}

func sendMessage(_ connection: NWConnection) {
    let data = "Example Send Data".data(using: .utf8)
    let completion = NWConnection.SendCompletion.contentProcessed { (error: NWError?) in
        print("送信完了")
    }
    connection.send(content: data, completion: completion)
}

startConnection()

while true {
    sleep(1)
}

最後のループとか格好悪い。

Server Source Java

package example.java.network;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class ExampleServerSocket {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(7777);
            System.out.println("socket create");
            Socket socket = serverSocket.accept();
            System.out.println("accept");
            Scanner scanner = new Scanner(socket.getInputStream());
            while(scanner.hasNext() == true) {
                System.out.println(scanner.nextLine());
            }
        }
        catch(Exception e) {
            System.out.println(e);
        }
    }
}

久々でScannerのhasNext()条件を忘れている。確認して改良しないと。

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

App内課金をSANDBOXユーザーでテストする - AppStoreConnect編(2019年)

最近App内課金を実装したので、忘れないようにまとめています。
AppStoreConnectのフォーマットはちょくちょく変わってるので、ハマりそうな箇所を記事に書いてます。

AppStoreConnectでやることは主に以下の3つです。

  • 契約/税金/口座情報の登録
  • マイAppでApp内課金の登録
  • テストユーザーの登録

参考にしたサイト

契約/税金/口座情報の登録

銀行口座納税フォーム連絡先の項目を入力していきます。

▼契約/税金/口座情報 / AppStoreConnect

スクリーンショット_2019-12-03_15_15_29.png

  • APP内課金をSANDBOXユーザーで試すには契約/税金/口座情報の登録は必須です!
  • ※最低でも「銀行口座」「納税フォーム」は入力しておかないとステータスがアクティブにならないため、(課金周りのテストができない)ので注意です。
  • 入力にはAdminFinance 以上の権限が必要です。 権限がない場合はAppStoreConnect上で上記のアイコンが表示されないで注意です。

▼有料App / 契約/税金/口座情報

スクリーンショット_2019-12-03_15_27_26.png

▼銀行口座

スクリーンショット_2019-12-03_16_47_17-3.png

▼納税フォーム

スクリーンショット_2019-12-03_16_47_26-2.png

▼連絡先

スクリーンショット_2019-12-03_16_47_35.png

  • 所属先に下記役割の方がいない場合は同一のユーザーでも大丈夫そうです。

▼契約

ステータスがアクティブになれば準備は完了です。
スクリーンショット_2019-12-03_15_27_26-2.png

マイAppでApp内課金の登録

▼マイApp

スクリーンショット_2019-12-03_15_15_29-2.png

▼新規Appの追加

スクリーンショット_2019-12-03_17_43_13.png

  • 新規Appの追加にはAdminAccountHolder以上の権限が必要です。

▼App内課金の追加

  • 機能>App内課金>(+) スクリーンショット_2019-12-03_17_51_51.png

▼App内課金のタイプを選択

スクリーンショット 2019-12-03 18.11.01.png

▼参照名と製品IDを入力

スクリーンショット_2019-12-03_18_22_28.png

  • 製品IDはコードで実装するProductIDと一致するものになります。
  • 私はアプリ名.製品名で登録しました。 ベストプラクティスがあれば教えてください。

▼ローカリゼーション

スクリーンショット_2019-12-03_19_28_13.png

  • AppStoreに表示される名前になります。

▼App Store プロモーション(オプション)

スクリーンショット_2019-12-03_20_17_15.png

スクリーンショット_2019-12-03_20_19_15.png

  • プロモーションを登録するとアプリのプロダクトページからApp内課金を表示や購入することができます。

▼WIP?‍♂️

さいごに(WIP)?‍♂️

WIP?‍♂️

コードで実装する編も書く予定です。?‍♂️

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

App内課金をSANDBOXユーザーでテストする - AppStoreConnect編(2019年版)

最近App内課金を実装したので、忘れないようにまとめています。
AppStoreConnectのフォーマットはちょくちょく変わってるので、ハマりそうな箇所を記事に書いてます。

AppStoreConnectでやることは主に以下の3つです。

  • 契約/税金/口座情報の登録
  • マイAppでApp内課金の登録
  • テストユーザーの登録

参考にした記事

契約/税金/口座情報の登録

銀行口座納税フォーム連絡先の項目を入力していきます。

▼契約/税金/口座情報 / AppStoreConnect

スクリーンショット_2019-12-03_15_15_29.png

  • APP内課金をSANDBOXユーザーで試すには契約/税金/口座情報の登録は必須です!
  • ※最低でも「銀行口座」「納税フォーム」は入力しておかないとステータスがアクティブにならないため、(課金周りのテストができない)ので注意です。
  • 入力にはAdminFinance 以上の権限が必要です。 権限がない場合はAppStoreConnect上で上記のアイコンが表示されないで注意です。

▼有料App / 契約/税金/口座情報

スクリーンショット_2019-12-03_15_27_26.png

▼銀行口座を入力

スクリーンショット_2019-12-03_16_47_17-3.png

▼納税フォームを入力

スクリーンショット_2019-12-03_16_47_26-2.png

▼連絡先を入力

スクリーンショット_2019-12-03_16_47_35.png

  • 所属先に下記役割の方がいない場合は同一のユーザーでも大丈夫そうです。

▼契約

ステータスがアクティブになれば準備は完了です。
スクリーンショット_2019-12-03_15_27_26-2.png

マイAppでApp内課金の登録

▼マイApp

スクリーンショット_2019-12-03_15_15_29-2.png

▼新規Appの追加

スクリーンショット_2019-12-03_17_43_13.png

  • 新規Appの追加にはAdminAccountHolder以上の権限が必要です。

▼App内課金の追加

  • 機能>App内課金>(+) スクリーンショット_2019-12-03_17_51_51.png

▼App内課金のタイプを選択

スクリーンショット 2019-12-03 18.11.01.png

▼参照名と製品IDを入力

スクリーンショット_2019-12-03_18_22_28.png

  • 製品IDはコードで実装するProductIDと一致するものになります。
  • 私はアプリ名.製品名で登録しました。 ベストプラクティスがあれば教えてください。

▼ローカリゼーションを入力

スクリーンショット_2019-12-03_19_28_13.png

  • AppStoreに表示される名前になります。

▼App Store プロモーション(オプション)を入力

スクリーンショット_2019-12-03_20_17_15.png

スクリーンショット_2019-12-03_20_19_15.png

  • プロモーションを登録するとアプリのプロダクトページからApp内課金を表示や購入することができます。

▼審査に関する情報を入力

スクリーンショット_2019-12-03_21_18_42.png

  • 製品のスクリーンショット or 購入ページのスクリーンショットを追加します。

テストユーザーの登録(WIP)?‍♂️

▼ユーザーとアクセス

スクリーンショット_2019-12-03_15_15_29.png

▼SANDBOX テスターを追加スクリーンショット_2019-12-03_21_27_30.png

▼テスターアカウントを追加

スクリーンショット 2019-12-03 21.30.30.png

  • メールアドレスはhoge@hoge.comでも通ります。実際に作成しているhoge@gmail.comでなくても大丈夫です。
  • 複数アカウントを作る際は連番で作成しました。hoge1@hoge.comhoge2@hoge.comなど
  • パスワードは8文字以上+英数字+英字大文字+小文字が必要になります。

▼SANDBOXユーザーでログイン

IMG_0143.png

  • 設定>itunes StoreとApp Store>SANDBOXアカウントでサインインできます。
  • もしくは購入ボタン押下時、ログインしていない場合は、ログインを要求するダイアログが出るので、ダイアログからサインインできます。
// 購入ボタンを押した時の購入処理
SKPaymentQueue.default().add(SKPayment(product: productID)

WIP?‍♂️

コードで実装する編も書く予定です。?‍♂️

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

enumを利用して複数の識別子を受け付けるAPIを呼び出す

userIDやUUIDを同じパラメータとして扱い、サーバー側でよしなに判別するAPIを呼び出す時にどのように書けば良いかを考えていきます。

GET /v1/users/{user_identity}

このAPIのuser_identityは次のようにuserのidでもUUIDでも呼び出すことができます。

GET /v1/users/123456
GET /v1/users/AAAAA-BBBB-CCCCC-DDDD

このAPIを呼び出すために2つのメソッドを定義しました。

func fetchUser(byID id: Int) {
  request(url: "/v1/users/\(id)")
}

func fetchUser(byUUID uuid: String) {
  request(url: "/v1/users/\(uuid)")
}

内部の処理は同じなので、次のように一つのメソッドにまとめてみます。

func fetchUser(by id: Int? = nil, uuid: String? = nil) {
    if id == nil && uuid == nil {
        fatalError()
    }
    if id != nil && uuid != nil {
        fatalError()
    }
    if let id = id {
        request(url: "/v1/users/\(id)")
    } else if let uuid = uuid {
        request(url: "/v1/users/\(uuid)")
    }
}

fetchUserは、idかuuidを埋めれば適切にapiを呼ぶようになりました。
ただし、idとuuidはどちらか一つを与えるようにしなくてはfatalErrorが発生します。

fetchUser(uuid: userUUID) //OK
fetchUser() // Crash
fetchUser(by: userID, uuid: userUUID) // Crash

あなたは実装者なのでこのパラメータの与え方のルールを知っていますが、他の開発者は中のコードを読むか、あなたが書いたコメントを読むか、はたまた間違えて呼んでクラッシュするのを確認しないと、気がつくことは出来ません。
不正な呼び出しをコンパイルの時点で気が付けない事も良くないですね。

そこで、Swiftの強力なenumを利用します。

enum UserIdentity {
    case id(Int)
    case uuid(String)
}

func fetchUser(by identity: UserIdentity) {
    switch identity {
        case .id(let id):
            request(url: "/v1/users/\(id)")
        case .uuid(let uuid):
            request(url: "/v1/users/\(uuid)")
    }
}

fetchUser(by .uuid(userUUID))

このようにenumで識別子を管理する事で、fetchUserを呼ぶには必ず任意の識別子を一つ指定する制約が生まれました。
間違えた呼び方をしてビルドすることはもう出来ません。


おまけ

今回のケースでは、APIのパスをenumが生成することで次のように書く事もできます。

enum UserIdentity {
    case id(Int)
    case uuid(String)
}

extension UserIdentity {
    var path: String {
      switch identity {
        case .id(let id):
          return "/v1/users/\(id)"
        case .uuid(let uuid):
          return "/v1/users/\(uuid)"
      }
    }
}

func fetchUser(by identity: UserIdentity) {
    request(url: identity.path)
}

fetchUser(by .uuid(userUUID))

メソッドの内部実装をシンプルにする事で、可読性が上がり処理が追いやすくなります。
これが正解かはプロジェクトの状況に寄りますが、可読性の高いコードを追求してみましょう。

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

iOSでFirebaseを使用して、データベースに対してCRUDを試してみる。

Xcodeで新規プロジェクトを作成しよう

Create a new Xcode project -> SingleViewApp の順で新規プロジェクトを作成する。
UserInterfaceはStoryBoardにしておきましょう。

Firebaseのプロジェクトを作成しよう

Firebaseに下記のURLからGoogleアカウントでログインします。
Firebase

ログインできたら「プロジェクトを追加」ボタンでプロジェクトを追加しましょう。

Firebaseの設定をしよう

プロジェクトのコンソール画面からアプリを追加することができます。
今回は「iOS」を選択しましょう。
80edd941.png

アプリのBundleIDを入力します。アプリ名は適当で良いです。
a6627889.png

設定ファイルをダウンロードして、Xcodeプロジェクトに追加します。

その後プロジェクトフォルダまで移動して、ターミナルから、下記コマンドを打ちましょう。

pod init

CocoaPodsをインストールしていない方は、インストールしてから行ってください。
こちらが参考になります。CocoaPodsを導入してみた - Qiita

今回はRealTimeDatabase を使用するので、Podファイルに、以下の文言を追加しましょう。

pod 'Firebase/Core'
pod 'Firebase/Database'

ターミナルから、下記コマンドでSDKをインストールしましょう。

pod install

インストールが完了したら、白いプロジェクトファイルが出来上がっていると思いますので、Xcodeで開きます。
97c2bdae.png

AppDelegeteファイルに、画像のように追記します。![スクリーンショット 2019-12-03 15.22.18.png]18a76768.png

データベースを作成しよう

左のタブのDataBaseを選択し、RealTimeDatabaseを選択します。
今回はテストモードで開始しましょう。
スクリーンショット 2019-12-03 15.47.01.png

5859c80c.png

これで準備完了です!

データベースに対して操作してみよう

データを追加してみよう

viewDidLoad に以下のように書いて実行します。

override func viewDidLoad() {
    super.viewDidLoad()
    let ref = Database.database().reference()

    // KeyValue型の配列を用意しておきます。
    let user = ["name":"Tarou Yamada", "age":"20", "favorite":"jogging"]

    // データを追加します。idは自動で設定してくれます。
    ref.child("Users").childByAutoId().setValue(user)
}

Firebaseのコンソール画面に移動すると、データが追加されているのが確認できます!
51ece462.png

データを更新してみよう

先程のデータを更新します。

override func viewDidLoad() {
    super.viewDidLoad()

    let ref = Database.database().reference()

    // 先程のIDを指定します。(人によって変わるので、自分のDatabaseからコピペしてね)
    let id = "-Lv9asdz8vsqeZSZQdyh"
    // 先程のIDを指定してデータを上書きします。
    ref.child("Users/\(id)/name").setValue("名無しの権兵衛")
}

データを削除してみよう

先程のデータを削除します。

override func viewDidLoad() {
    super.viewDidLoad()

    let ref = Database.database().reference()

    // 先程のIDを指定します。(人によって変わるので、自分のDatabaseからコピペしてね)
    let id = "-Lv9asdz8vsqeZSZQdyh"
    // 先程のIDを指定してデータを削除します。
    ref.child("Users/\(id)").removeValue()
}

データを取得してみよう

サンプルデータを作ろう

FireBaseのコンソール画面で、データを直接編集することができます。
めんどくさい人は、下記のJSONをファイルにして、FireBaseのインポートしましょう。

test.json
{
  "Users" : {
    "-Lv4cGsQbS3cXDLTErXS" : {
      "age" : "18",
      "favoriteFood" : "クッキー",
      "name" : "田中 花子"
    },
    "-Lv8Lrf3ii9bB5p6rcIc" : {
      "age" : "22",
      "favoriteFood" : "牛丼",
      "name" : "山田 太郎"
    },
    "-Lv8gSwpG-ZkEbPsbORu" : {
      "age" : "13",
      "favoriteFood" : "たこ焼き",
      "name" : "木村 正人"
    },
    "-Lv8vAUr79kcpoygkpDl" : {
      "age" : "26",
      "favoriteFood" : "寿司",
      "name" : "川上 純子"
    }
  }
}

サンプルデータを取得してみよう

下記コードで、Xcodeのコンソールに取得結果が表示されます。

override func viewDidLoad() {
    super.viewDidLoad()

    let ref = Database.database().reference()

    // データの変更を監視(observe)してるため、変更されればリアルタイムで実行されます。
    ref.child("Users").observe(.value) { (snapshot) in
        // Users直下のデータの数だけ繰り返す。
        for data in snapshot.children {
            let snapData = data as! DataSnapshot

            // Dictionary型にキャスト
            let user = snapData.value as! [String: Any]
            print(user)
        }
    }
}

まとめ

環境の設定から、CRUDの基本的なところまでは押さえられたはず。 次はFirebaseを使って簡単なアプリを作ってみたい。

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

Swiftのプロパティを三つの分類で整理してみた

OSSのコードを読んでいたり、他の人の講演やLTを聞いていると、あれ?lazyってなんだったっけ?と、「プロパティについてちゃんと理解していないよな。。。」って思うことがよくあります。
今回はそんなプロパティについてまとめましたので、同じような悩みをもった方の参考となれば嬉しいです。

対象とする読者

  • プロパティについてなんとなく使えるけど、聞かれると困る人
  • lazyってなんだっけ?コンピューテッドプロパティってなんだっけ?って人

目次

  • プロパティの分類方法
  • 分類①:定数と変数
  • 分類②:インスタンスプロパティとスタティックプロパティ
  • 分類③:ストアドプロパティとコンピューテッドプロパティ

検証環境

  • Swift : 5.0.1
  • Xcode : 11.0(Playground)

プロパティの分類方法

プロパティとは

型に紐づく値のこと。プロパティと言ったり、属性と言ったりする。
型はプロパティとメソッド(関数/ふるまい)で構成される。

分類①:定数と変数

値の再代入が不可能な定数と、再代入が可能な変数での分け方。letとvar。

分類②:インスタンスプロパティとスタティックプロパティ

インスタンスに紐づくプロパティと型自身に紐づくプロパティでの分類。
インスタンスによらず、型共通の値をもたせたいときには、スタティックプロパティを用いる。
スタティックプロパティにはlet/varの前にstaticを付ける。

分類③:ストアドプロパティとコンピューテッドプロパティ

値を保持するプロパティと値を都度計算するプロパティ
値を保持するストアドプロパティに対して、コンピューテッドプロパティは既存のプロパティを用いて都度用意されるため、計算もととの整合性が常に保たれるという特徴がある。

分類①:定数と変数

プロパティは値の名前の前に必ずletかvarを付けて、プロパティが定数か変数かを宣言しなければならない。変数には値の再代入が可能だが、定数への値の再代入は不可能。

struct Message {
    let greeting = "Hello," // 定数
    var name = "Everyone!"  // 変数
}

var message = Message()
message.name = "@ichikawa7ss!" // 変数には再代入可能だが
message.greeting = "Goodbye,"  // 定数には再代入不可能 -> コンパイルエラー

分類②:インスタンスプロパティとスタティックプロパティ

インスタンスに紐づくプロパティと型自身に紐づくプロパティでの分類。

次のソースコードでの例のように、インスタンスプロパティではプロパティがインスタンスに紐づいているので、インスタンスごとにプロパティの設定が可能。

struct Message {
    let greeting = "Hello," // インスタンスプロパティ(定数)
    var name = "Everyone!"  // インスタンスプロパティ(変数)
}

// Message構造体のインスタンスを生成
var message1 = Message()
let name1 = message1.name        // Everyone!

// 別のMessage構造体のインスタンスを生成
var message2 = Message()
// インスタンスプロパティに値を代入
message2.name = "@ichikawa7ss!"
let name2 = message2.name        // @ichikawa7ss!

一方でスタティックプロパティでは、プロパティが型自身に紐づくため、インスタンスによらずプロパティの値は一定である。
また、呼び出し方法もインスタンスプロパティがインスタンス名.プロパティ名であるのに対して、型に紐づくスタティックプロパティは型名.プロパティ名で値の呼び出しを行う必要がある。

struct Student {
    static let className = "3-A"          // スタティックプロパティ
    static var teacherName = "Ms.Tanaka"  // スタティックプロパティ
    var name = "Shoma Ichiakwa"           // インスタンスプロパティ
}

// 一つ目のインスタンスを生成
var student1 = Student()
let teacherName1 = Student.teacherName  // Ms.Tanaka (スタティックプロパティ)
let name1 = student1.name               // Shoma Ichiakwa

// 別のインスタンスを生成
var student2 = Student()
// インスタンスプロパティ(インスタンスに紐づくプロパティ)に値を代入
student2.name = "Taro Yamada"
// スタティックプロパティも変数であれば変更可能
// スタティックプロパティは'型.プロパティ名'で値を呼び出す
Student.teacherName = "Ms.Suzuki"

let teacherName2 = Student.teacherName  // Ms.Suzuki (スタティックプロパティ)
let name2 = student2.name               // Taro Yamada

Student.className = "3-B"               // 定数は再代入不可能 -> コンパイルエラー

letとstaticは最初混同するかもしれないが、staticは型に共通の変数を持たせるだけなので、値の変更は可能。

分類③:ストアドプロパティとコンピューテッドプロパティ

ストアドプロパティ

これまで見てきたプロパティはインスタンスや型に値を保持することができた。これをストアドプロパティという。一方で値を保持せずにアクセスするたびに値を計算するプロパティをコンピューテッドプロパティと呼ぶ。
まずはストアドプロパティに特有なプロパティオブザーバーいう機能とレイジーストアドプロパティについて確認する。

プロパティオブザーバー

ストアドプロパティにはプロパティオブザーバーという、値の変更を監視する処理を定義することができる。(コンピューテッドプロパティは変更前の値を保持しないので、変更の監視という考え方自体が存在しない。)
以下は、インスタンス化したStudentのnameプロパティに値が更新された際に、オブザーバープロパティが機能している例。

struct Student {
    static let className = "3-A"
    // プロパティオブサーバーの用いてプロパティの変更を監視
    var name = "Shoma Ichiakwa" { // ストアドプロパティ
        // 値変更の"直前"に実行
        willSet {
            // 代入された値は'newValue'として使用可能
            print("**Student name will chenge to \(newValue) from \(name)**")
        }
        // 値変更の"直後"に実行
        didSet {
            // didSetの場合は代入された値はプロパティを指定して呼び出し
            print("**Student name had chenged to \(name)**")
        }
    }
}

// Student構造体のインスタンスを作成
var student = Student()
print("Created Instance: \(student)")
student.name = "Taro Yamada"
print("Student name: \(student.name)")

この結果は以下の通り。

【実行結果】
Created Instance: Student(name: "Shoma Ichiakwa")
**Student name will chenge to Taro Yamada from Shoma Ichiakwa**
**Student name had chenged to Taro Yamada**
Student name: Taro Yamada

インスタンス生成後、studentのnameプロパティに値の変更を行うとwillSet->didSetの順に呼び出される。

レイジーストアドプロパティ

varの前にlazyをつけることで、レイジーストアドプロパティを宣言することができる。
通常のプロパティはインスタンスが生成され初期化されるときに値が取得されるのに対して、レイジーストアドプロパティではインスタンス生成後、実際にプロパティへのアクセスが行われるまで値の取得を遅延できる。
これにより初期化コストの高いプロパティの初期化を遅延させることができるため、アプリケーションのパフォーマンス向上に有効。
ちなみにコンピューテッドプロパティでは、そもそも毎回アクセスのたびに値が取得されるため、アクセスを遅延させる必要がない。

struct SomeStruct {

    // ストアドプロパティ
    var value : Int = {
        print("** Accessed and set 'value' **")
        return 1
    }()

    // レイジーストアドプロパティ
    lazy var lazyValue: Int = {
        print("** Accessed and set 'lazyValue' **")
        return 2
    }()
}

var someStruct = SomeStruct()
print("Instance Created")
print("value is \(someStruct.value)")
print("lazyValue is \(someStruct.lazyValue)")

この出力結果は以下の通り。

【実行結果】
** Accessed and set 'value' **
Instance Created
value is 1
** Accessed and set 'lazyValue' **
lazyValue is 2

インスタンス作成時にはlazyValueへのアクセスは遅延されており、値をプリントするときに初めてlazyValueへのアクセスが行われている。

また以下の例のように、レイジーストアドプロパティはインスタンス生成後にアクセスされるため、他のストアドプロパティを使用することができる。

struct SomeStruct {
    // ストアドプロパティ
    var value = 1
    // ストアドプロパティ
    var notLazyDoubleValue: Int = {
        return value * 2 // 通常のストアドプロパティは初期化時に他のプロパティを使うことができない -> コンパイルエラー
    }()
    // レイジーストアドプロパティ
    lazy var lazytripledValue: Int = {
        return value * 3 // レイジーストアドプロパティはインスタンス生成後にアクセスされるため、他のプロパティを使用可能
    }()
}

コンピューテッドプロパティ

コンピューテッドプロパティはすでにあるプロパティを用いて、アクセスするたびに計算を行うため、計算元のプロパティと常に値の整合性が取れることが特徴。
コンピューテッドプロパティには値を呼び出すときに、他のプロパティの値を用いて値を返してくれるゲッタと、プロパティに値が代入されたときに、他のプロパティを更新してくれるセッタが存在する。

ゲッタ

ゲッタではプロパティにアクセスしたときに他のプロパティを用いて、値を返却してくれる。以下の例ではmessage.signatureにアクセスするたびにgetの処理が走り、toプロパティを用いて値を計算する。

struct Message {
    var to : String
    // コンピューテッドプロパティ
    var signature : String {
        // ゲッタを使用して、他のプロパティを用いた値の取得が可能
        get {
            return "Dear,\(String(describing: to))"
        }
    }
    init(to: String) {
        self.to = to
    }
}

let message = Message(to: "Ms.Tanaka")
message.signature // Dear,Ms.Tanaka

セッタ

セッタを定義されたプロパティは、プロパティに値が代入されると、setのスコープ部に書かれた処理を実行する。これにより他のプロパティの更新処理ができるため、ゲッタと併用することで、相関のある二つのプロパティ間での整合性を記述することが可能。
以下の処理ではkmとmという二つの距離単位をゲッタとセッタを用いることで二変数間の整合性を保っている。

struct Distance {
    var kirometers : Int = 0

    // コンピューテッドプロパティ
    var meters : Int {
        // ゲッタを使用して、他のプロパティを用いた値の取得が可能
        get {
            return kirometers * 1000
        }
        // セッタを使用して、他のプロパティを更新することが可能
        // セットされた値を変数として命名して利用可能
        set(inputMeters) {
            kirometers = inputMeters / 1000
        }
    }
}

var distanceStations = Distance()
distanceStations.kirometers = 2
distanceStations.meters          // 2000

var distanceSchoolAndHome = Distance()
distanceSchoolAndHome.meters = 5000
distanceSchoolAndHome.kirometers // 5

distanceSchoolAndHome.meters = 5000のようにと値を格納するので、直観的にはプロパティが値を保持しているように思えるが、実際にはsetキーワード定義された処理を行なっているだけで、値は保持されていない。
kirometersに値が代入されようと、metersに値が代入されようと、結果的に値の更新・保持がなされるのはストアドプロパティであるkirometersである。metersの値を取得するときにはget内の処理により、kirometersを通して正しいmetersの値取得を行なっている。

まとめ

プロパティ、ややこしくて今まで避けてきましたが、Swiftの基礎なだけに一度整理することで様々な領域の理解に繋がると感じました。三つの分類に分けて考えると、役割の明確化なプロパティの宣言ができるようになりそうです。

参考

[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plus)
(いつも大変お世話になっております?‍♂️)

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

ARKITで顔認識(ARKIT2.0のFaceTrackingが使えないiphone7にて。)

swift初心者です。(というかプログラム初心者。)
いろいろ言語を学んでいるところなので、swiftの勉強記録ということで後で見返せるように投稿しておこうと思います。

ARで顔認識するアプリ作ってみたいと思ったのが発端で、調べたところ、iphone X以上なら以下の機能でAR用の顔認識が使えることがわかりました。
Tracking and Visualizing Faces
でも自分はiphone7と8しか持ってないので、iphone7でも使える2次元用の顔認識のVISIONを組み合わせて実現してみることにしました。

作ったもの

「iphoneのカメラを人物に向けると、人物の頭上に球体が表示され、その球体をタッチするとその人物にARサングラスをかける」

output6.gif

コード

import UIKit
import SceneKit
import ARKit
import Vision

class ViewController: UIViewController, ARSCNViewDelegate {

    var condition = "OFF"
    var originalFrame:CVPixelBuffer?
    var finalImage:CIImage?
    var results:[VNFaceObservation]?
    var faceHeight:CGFloat?
    var midiumX:CGFloat?
    var midiumY:CGFloat?
    var screenPosition:SCNVector3?
    let screenWidth: CGFloat = UIScreen.main.bounds.size.width
    let screenHeight: CGFloat = UIScreen.main.bounds.size.height

    @IBOutlet var sceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set the view's delegate
        sceneView.delegate = self

        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true

        // Create a new scene
        let scene = SCNScene()

        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
        self.sceneView.addGestureRecognizer(tapRecognizer)

        // Set the scene to the view
        sceneView.scene = scene

    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()

        // Run the view's session
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // Pause the view's session
        sceneView.session.pause()
    }

    // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        guard let currentFrame = self.sceneView.session.currentFrame?.capturedImage else {
            return
        }
        originalFrame = currentFrame
        convertImageForFaceDetection()
        faceDetection()
    }



    @objc func tapped(recognizer: UITapGestureRecognizer) {
        let sceneView = recognizer.view as! SCNView
        let location = recognizer.location(in: sceneView)
        let hitResults = sceneView.hitTest(location, options: [:])

        if  !hitResults.isEmpty{
            let node = hitResults[0].node
            let material = node.geometry?.material(named: "Color")
            if condition == "OFF"{
                condition = "ON"
                material?.diffuse.contents = UIColor.yellow
            }else{
                condition = "OFF"
                material?.diffuse.contents = UIColor.white
            }
        }
    }

    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user

    }

    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay

    }

    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required

    }
}

extension ViewController {

    func convertImageForFaceDetection() {
       //Pixelbuffer doesn't have rotate information and default angle is landscape. Need to convert to CIImage to fix these issues.
       let ciImage: CIImage = CIImage(cvPixelBuffer: originalFrame!).oriented(.right)
       let imageWidth = ciImage.extent.width
       let imageHeight = ciImage.extent.height

       //Original pixelbuffer size is 1920 * 1440. iphone7 bounds size iws 667* 350. which means aspect ratio is different. So adjusting the pixelbuffer size to iphone7 bounds size.
       let adjustSize = imageHeight / screenHeight
       let adjustedImageWidth = screenWidth * adjustSize

       let croppedPosition = floor((imageWidth - adjustedImageWidth) / 2)

       let finalSize = CGRect(x:croppedPosition, y:0 ,width:adjustedImageWidth, height:imageHeight)
       finalImage = ciImage.cropped(to: finalSize)
    }

    func faceDetection(){
        let faceDetectionRequest = VNDetectFaceRectanglesRequest()
        let faceDetectionHandler = VNSequenceRequestHandler()

        try? faceDetectionHandler.perform([faceDetectionRequest], on: finalImage!)
        guard let detectResults = faceDetectionRequest.results as? [VNFaceObservation] else {
            return
        }
        results = detectResults
        if results!.isEmpty {
            return
        } else {
            addingARSphere()
            //Touching existing sphere turns "condition" to "ON". then sunglass will appeared. Details of "condition" is in "func tapped".
            if condition == "ON" {
                addingARSunglasses()
            } else {
                //Touching existing sphere again turns "condition" to "OFF". then sunglass will disappeared.
                self.sceneView.scene.rootNode.enumerateChildNodes {(nodes, _) in
                    if nodes.name == "model"{
                        nodes.removeFromParentNode()
                    }
                }
            }
        }
    }

    func addingARSphere(){
        //Delete previous position's sphere before making new one.
        self.sceneView.scene.rootNode.enumerateChildNodes {(nodes, _) in
            if nodes.name == "normalSphere"{
                nodes.removeFromParentNode()
                }
        }

        //Sphere attribute.
        let sphereNode = SCNNode()
        faceHeight = floor(results![0].boundingBox.height * 1000)/1000
        let sphereSize = faceHeight! / 5
        sphereNode.geometry = SCNSphere(radius: sphereSize)
        sphereNode.name = "normalSphere"
        let sphereMaterial = SCNMaterial()
        sphereMaterial.name = "Color"
        if condition == "ON"{
            sphereMaterial.diffuse.contents = UIColor.yellow
        } else {
            sphereMaterial.diffuse.contents = UIColor.white
        }
        sphereNode.geometry?.materials = [sphereMaterial]

        //Getting camera position.
        guard let cameraNode = self.sceneView.pointOfView else { return }
        let toCameraPosition = SCNVector3(x: 0, y: 0, z: -0.3)
        let objectPosInWorld = cameraNode.convertPosition(toCameraPosition, to:nil)
        screenPosition = self.sceneView.projectPoint(objectPosInWorld)

        //The result of VNfaceobservationresult is ratio. so multiply bounds size. And result (0,0) is upper left. but iphone UI (0,0) is below left. y needs to be converted.
        midiumX = floor(results![0].boundingBox.midX * 1000)/1000
        midiumY = floor(1) - floor(results![0].boundingBox.midY * 1000)/1000
        screenPosition!.x = Float(screenWidth * midiumX!)
        screenPosition!.y = Float(screenHeight * midiumY!) - Float(screenHeight * faceHeight! * 2)
        //Converting 2D position to 3D position.
        let worldPosition = self.sceneView.unprojectPoint(screenPosition!)
        sphereNode.position = worldPosition

        self.sceneView.scene.rootNode.addChildNode(sphereNode)

    }

    func addingARSunglasses(){
    //No DispatchQueue makes warning when we use SCNscene in renederer. Btw actually it works without DispatchQueue. just for deleting warning.
        DispatchQueue.main.async{
            self.sceneView.scene.rootNode.enumerateChildNodes {(nodes, _) in
                if nodes.name == "model"{
                    nodes.removeFromParentNode()
                    }
            }
            let eyeScene = SCNScene(named: "sunglasses.scn")
            let sunglassesNode = eyeScene!.rootNode.childNode(withName: "model", recursively: true)

            self.screenPosition!.y = Float(self.screenHeight * self.midiumY! * 0.95)
            let worldPosition2 = self.sceneView.unprojectPoint(self.screenPosition!)
            sunglassesNode?.position = worldPosition2
            let eyeSize = self.faceHeight! * 0.2
            sunglassesNode?.scale = SCNVector3(eyeSize, eyeSize , eyeSize)
            self.sceneView.scene.rootNode.addChildNode(sunglassesNode!)
        }
    }
}

苦労した点と解決策

顔認識の位置が変な位置になっている時に何がおかしいのかわからず何度もprintするなどして苦労して気づいた点と解決策が以下。

・VISIONから返って来る座標(左下が(0,0))がスクリーン座標(左上が(0,0))と違う。
→y座標は反転させて処理する。

・VISIONから返って来る座標は相対位置(1が最大)
→画面サイズを掛け算して、絶対位置に変換する必要がある。

・スクリーン座標は0から1の相対位置ではなく絶対位置。
→スクリーン座標からワールド座標への変換(unprojectpoint)のときには絶対位置を用いる。

・ARSCNで取得したバッファーサイズは1920*1440なので、iphone7の画面サイズの比率(667*335)と違うため、顔認識の位置が微妙にずれる。ちなみにこれが一番意味不明な仕様だった。AVCaptureの方でバッファー取得したら1920*1080だった(iphone7の画面比率と同じ)のに。
→取得したバッファーの端を切り取る等して比率をiphone側に合わせる。

・ARSCNで取得したバッファーイメージは回転情報を持たないので、常に横長の画像になってる。
→縦にするには、回転情報を持ち、かつVISIONの対応フォーマットであるciImageにする。

・renderer内でSCNSceneをinitするとコンソール上でワーニングが出る。UIview関連をrenderer内でいじるのはお作法違反?らしい?
→dispatchqueueを使うのが対策らしいので使ってワーニングが出ないようにした。が、dispatchqueue使わずワーニングは出たとしてもアプリ自体は動く上にARモデルの位置変更もdispatchqueueなしの方が素早く行われる。ここは結局、なぜそういうお作法なのかよくわからなかった。

・3DModelをダウンロードする時に、scnがなかった。
→objファイルをダウンロードし、blenderでdaeにした後、xcodeでscnにした。(usdzならダウンロード可能であったが、usdzでいざAR表示してみると、3Dmodelが原型をギリギリ留めているレベルでめちゃくちゃ汚くなるので、scnで実装する方法にした。)

・3DModelのカメラの位置が前後逆?となっており、ARとして表示する時にサングラスが前後逆に表示されてしまった。
→ライト位置とカメラ位置をxcode上でも修正できる。

・scnシーンにあるARモデルをノードとして取り出す手段がよくわからず、何度も「nilなのでエラー」と言われ苦戦した。
→xcodeでscnファイルを選択して、3Dmodelを構成しているコンポーネントをまとめた上で名前をつける。その後、その名前をノードとして取り出すようにすると解決した。

参考文献

ARアプリの作り方として、
【6日で速習】iOS 13アプリ開発入門決定版 20個のアプリを作って学ぼう(Xcode 11, Swift 5対応中)

VISIONの使い方として、
[iOS 11] 画像解析フレームワークVisionで顔認識を試した結果
VNFaceObservation

ピクセルサングラスの3Dモデルは、
Pixel Sunglasses

3Dモデルをいじるやり方として、
ARKit を使って 3D モデルを AR で表示してみた

【その他感想】
完成までに2~3週間くらいかかった。
各変数の中身を何度もprintしたり公式ドキュメントや似たことをやってる別の方のブログを見たりして解決したけど、かなり大変だった。
公式ドキュメントがもうちょっと充実してても良いと思うんだけどなあ。せめて例くらい載せてほしい。

xcodeは超便利だと思った。型の不一致や宣言ミスなどの凡ミスがあった場合に、説明付きで指摘してくれる+修正候補をくれるところが。

そして、作り終えてから思ったけど、あんまりAR感がない・・・。
あと、新しい機能って、ユーザー側に対応端末が普及してないと宝の持ち腐れになるなーと思いました。

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

【Xcode】複数プロジェクトでのソースの共通化

複数のプロジェクトでソースを共通化する

Xcodeは英語だらけなのでシェアしておく

共通化とは

ここでいう共通化は参照のことです。
元となるAというプロジェクトのソースを、他のプロジェクトBから参照する方法です。

やり方

  1. 参照先のプロジェクトを開く
  2. ProjectNavigatorに参照元のプロジェクトから参照したいファイルをドラッグ&ドロップ
  3. コピーするか参照するかを選択するダイアログがでるので「Create folder reference」にチェックをいれてfinishを押す
  4. これで参照の設定が完了です。

ソースをいじった場合に、参照されているすべてのプロジェクトで変更がかかるようになります。
プロジェクトを複製してコピーアプリを作成する場合などに使いましょう。

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

iOSで開発/本番環境を分けてFirebaseを運用するTips

iOS Advent Calendar 2019の4日目の@shtnkgmです!

本記事ではiOSで開発/本番環境を分けてFirebaseを利用するTipsを紹介します。
環境を分けて利用したいユースケースは以下のようなものが挙げられます。

  • 開発/本番環境によって運用者を分けたい
    例)開発環境と本番環境を利用するチームが異なる

  • 開発環境を本番環境と区別することで本番環境に影響なく開発をしやすくしたい
    例)テスト時に開発環境へのPush通知を間違って本番環境に送らないようにしたい

  • 本番環境の計測数値に開発時のものが含まれないようにしたい
    例)AnalyticsやPerformance Monitoring, Crashlyticsの分析精度を高めたい

実現方法

Firebaseで開発/リリース環境を分ける

まずはFirebase側の環境を分ける必要があります。Firebase側の環境を分けるには、以下のいずれかの方法で実現できます。

  • Firebaseプロジェクトを環境毎に複数作成する
  • 同プロジェクト内にアプリを環境毎に複数作成する

image.png

プロジェクト単位で分割した場合はユーザー権限設定も個別に行えますが、アナリティクスは同時にまとめて見られなくなるなどの違いがあるので、運用方法を想定して決めると良いかと思います。

GoogleService-Info.plistを環境毎に分ける

通常の設定ではセットアップ時にFirebase ConsoleからGoogleService-Info.plistという構成ファイルをダウンロードし、それがデフォルトで読み込まれます。

環境毎に構成ファイルは異なるため、構成ファイルを切り替える必要があり、以下の2種類の方法があります。

  • 環境毎のGoogleService-Info.plistを別々のディレクトリに配置し、Target MembershipによってTargetで切り替える
  • 環境毎のGoogleService-Info.plistを別々のファイル名にする

Targetが環境によって別れていれば前者でも問題ありませんが、後者のファイル名を区別する方法が分かりやすいためオススメです。

別々のファイル名にする例)

  • GoogleService-Info-dev.plist(開発環境)
  • GoogleService-Info-qa.plist(QA環境)
  • GoogleService-Info-release.plist(本番環境)

注意

ファイル名で区別する場合は、デフォルトのGoogleService-Info.plistというファイル名は利用しないほうが良さそうです。
公式ドキュメントのFirebase / アナリティクスのレポートの信頼性を確保するアナリティクスが失われる可能性があると記述があります。

環境毎に構成ファイルを指定する

環境毎にTargetが分かれている場合は、Build SettingsのPreprocessor Macrosのマクロで以下のように構成ファイル名を分岐させると良いです。

let configFileName: String
#if DEBUG
configFileName = "GoogleService-Info-dev"
#elseif QA
configFileName = "GoogleService-Info-qa"
#else
configFileName = "GoogleService-Info-release"
#endif

構成ファイルは以下のようにconfigure(options:)メソッドで指定できます。

guard let filePath = Bundle.main.path(forResource: "Firebase/\(configFileName)", ofType: "plist"),
    let options = FirebaseOptions(contentsOfFile: filePath) else {
        fatalError("Firebase plist file is not found.")
}
FirebaseApp.configure(options: options)

CrashlyticsへのdSYMアップロード

クラッシュ分析用にFirebase Crashlyticsを利用している場合、シンボルファイル(dSYM)をアップロードする必要があります。
アップロード処理はFastlaneで自動化すると便利です。

Fastlaneではupload_symbols_to_crashlyticsというアクションが利用できます。
以下のように、アクションのパラメータに構成ファイルのパスやdSYMファイルの保存先を指定します。

upload_symbols_to_crashlytics(
  plistName = configuration == "Release" ? "GoogleService-Info-release.plist" : "GoogleService-Info-qa.plist"
  gsp_path: "./YourApp/Path/#{plistName}",
  dsym_path: ENV['DSYM_PATH']
)
  • gsp_pathには環境毎の構成ファイルのパスを指定します。
  • dsym_pathにはCI環境でビルドした際のdSYMファイルの保存パスを指定します。利用しているCIサービスによっては環境変数として与えられていることもあります。

おわりに

本記事ではiOSで開発/本番環境を分けてFirebaseを運用するTipsについて紹介しました。

Firebaseの環境を複数に分けると、設定を環境毎に柔軟に行える反面、同じ設定をFirebase Consoleで重複して行う必要があるので、手間を減らすいい方法がないかなと思っています。
本記事と関連する内容で何かアドバイスやご意見があればコメントに記載いただけると嬉しいです。

以上iOS Advent Calendar 2019の4日目の記事でした。

参考

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

SwiftUIでNukeを使ってリモートの画像を表示する(Nuke)

はじめに

SwiftUIを使い始めました。全てSwiftで宣言的にLayoutを記述でき、ReactiveXを使わなくてもデータをバインディングできて、すごくいいなと感じています!

さて、アプリを作るときは多くの場合画像ライブラリを使って画像を表示すると思います。私はNukeを使っているので、ここでは、Nukeを使ってリモートの画像を表示するSwiftUIのCustomViewを作成する方法を説明します。(Nuke以外も同じようにできると思います。)

CustomView

RemoteImageView.swift
import SwiftUI

struct RemoteImageView: View {
    @ObservedObject var remoteImage: RemoteImage
    var body: some View {
        Image(uiImage: remoteImage.image)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}

struct RemoteImageView_Previews: PreviewProvider {
    static var previews: some View {
        RemoteImageView(remoteImage: RemoteImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/JRE-E231-500-for-JRyamanote-line.jpg/2560px-JRE-E231-500-for-JRyamanote-line.jpg")!))
    }
}

ObservableObject

ここで画像を読み込みます。
imageが変わるたびに変更がpublishされ、上のCustomViewに反映されます。

RemoteImage.swift
import Foundation
import SwiftUI

import Nuke

class RemoteImage: ObservableObject {
    @Published var image: UIImage

    private let url: URL

    init(url: URL, placeholder: UIImage = UIImage()) {
        self.image = placeholder
        self.url = url

        loadImage()
    }

    private func loadImage() {
        ImagePipeline.shared.loadImage(with: url) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let response):
                self.image = response.image
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}

使い方

SwiftUIのプロジェクトを作成すると自動生成されるContentViewで、RemoteImageViewを利用します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        RemoteImageView(remoteImage: RemoteImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/JRE-E231-500-for-JRyamanote-line.jpg/2560px-JRE-E231-500-for-JRyamanote-line.jpg")!))
    }
}

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

表示はこんな感じです。山手線です。

スクリーンショット

以上です。

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

SwiftUIでNukeを使ってリモートの画像を表示する

はじめに

SwiftUIを使い始めました。全てSwiftで宣言的にLayoutを記述でき、ReactiveXを使わなくてもデータをバインディングできて、すごくいいなと感じています!

さて、アプリを作るときは多くの場合画像ライブラリを使って画像を表示すると思います。私はNukeを使っているので、ここでは、Nukeを使ってリモートの画像を表示するSwiftUIのCustomViewを作成する方法を説明します。(Nuke以外も同じようにできると思います。)

CustomView

RemoteImageView.swift
import SwiftUI

struct RemoteImageView: View {
    @ObservedObject var remoteImage: RemoteImage
    var body: some View {
        Image(uiImage: remoteImage.image)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}

struct RemoteImageView_Previews: PreviewProvider {
    static var previews: some View {
        RemoteImageView(remoteImage: RemoteImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/JRE-E231-500-for-JRyamanote-line.jpg/2560px-JRE-E231-500-for-JRyamanote-line.jpg")!))
    }
}

ObservableObject

ここで画像を読み込みます。
imageが変わるたびに変更がpublishされ、上のCustomViewに反映されます。

RemoteImage.swift
import Foundation
import SwiftUI

import Nuke

class RemoteImage: ObservableObject {
    @Published var image: UIImage

    private let url: URL

    init(url: URL, placeholder: UIImage = UIImage()) {
        self.image = placeholder
        self.url = url

        loadImage()
    }

    private func loadImage() {
        ImagePipeline.shared.loadImage(with: url) { [weak self] result in
            guard let self = self else { return }
            switch result {
            case .success(let response):
                self.image = response.image
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}

使い方

SwiftUIのプロジェクトを作成すると自動生成されるContentViewで、RemoteImageViewを利用します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        RemoteImageView(remoteImage: RemoteImage(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/JRE-E231-500-for-JRyamanote-line.jpg/2560px-JRE-E231-500-for-JRyamanote-line.jpg")!))
    }
}

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

表示はこんな感じです。山手線です。

スクリーンショット

以上です。

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

SwiftUI Christmas?

今年もクリスマスが近いてきました。

昨年はCore Animationを使ってクリスマスツリーを作成してみました。
https://qiita.com/shiz/items/10cb712a26620f2e3bdc

そこで
今年はSwiftUIを使ってクリスマスツリーを作成したいと思います。

SwiftUIの要素はたくさん使用していますが
drawingGroupに注目したいと思います。

Shapeに適合させツリーのパーツを作成する

まずツリーに必要なパーツを作っていきます。
図形をShapeプロトコルに適合させたstructとして定義して
それを組み合わせてViewを構築します。

スクリーンショット 2019-12-01 10.46.06.png

こちらは下記のサイトを参照させて頂きました。
https://www.hackingwithswift.com/quick-start/swiftui/how-to-draw-polygons-and-stars

pathメソッドの中でPathクラスを生成します。
Pathの指定はUIBezierPathと似たような形で設定できます。


コード
struct Star: Shape {
    let corners: Int
    let smoothness: CGFloat

    func path(in rect: CGRect) -> Path {
        guard corners >= 2 else { return Path() }

        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)

        var currentAngle = -CGFloat.pi / 2

        let angleAdjustment = .pi * 2 / CGFloat(corners * 2)

        let innerX = center.x * smoothness
        let innerY = center.y * smoothness

        var path = Path()

        path.move(to: CGPoint(x: center.x * cos(currentAngle), y: center.y * sin(currentAngle)))

        var bottomEdge: CGFloat = 0

        for corner in 0..<corners * 2  {
            let sinAngle = sin(currentAngle)
            let cosAngle = cos(currentAngle)
            let bottom: CGFloat

            if corner.isMultiple(of: 2) {
                bottom = center.y * sinAngle
                path.addLine(to: CGPoint(x: center.x * cosAngle, y: bottom))
            } else {
                bottom = innerY * sinAngle
                path.addLine(to: CGPoint(x: innerX * cosAngle, y: bottom))
            }
            if bottom > bottomEdge {
                bottomEdge = bottom
            }
            currentAngle += angleAdjustment
        }
        let unusedSpace = (rect.height / 2 - bottomEdge) / 2
        let transform = CGAffineTransform(translationX: center.x, y: center.y + unusedSpace)
        return path.applying(transform)
    }
}


次に木の部分です。

スクリーンショット 2019-12-01 10.46.46.png

木の形の部分

まずは緑の部分の三角形Triangleを定義します。


コード
struct Triangle: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            let middle = rect.midX
            let width: CGFloat = rect.size.width
            let height = rect.height
            path.move(to: CGPoint(x: middle, y: 0))
            path.addLine(to: CGPoint(x: middle + (width / 2), y: height))
            path.addLine(to: CGPoint(x: middle - (width / 2), y: height))
            path.addLine(to: CGPoint(x: middle, y: 0))
        }
    }
}


白い線状の飾り

木の上にある飾りを作成します。

今回はaddQuadCurveを使用して
ちょっと曲線にしています。

https://developer.apple.com/documentation/swiftui/path/3271274-addquadcurve


コード
struct Slope: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: rect.minX, y: rect.midY))
            path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY),
                              control: CGPoint(x: rect.midX * 0.8, y: rect.midY * 0.8))
        }
    }
}


球状の飾り

次に球状の飾りを作ります。
これはCircle
GradientLinierGradientを使って
色をグラデーションしています。

https://developer.apple.com/documentation/swiftui/gradient
https://developer.apple.com/documentation/swiftui/lineargradient

Gradientの初期化時にグラデーションさせたい色を指定します。
変化させる位置を直接指定することもできますが
指定しない場合は
フレームワークで自動で調整してくれるようです。

LinearGradient
Gradientと開始と終了位置を指定します。


コード
struct BallView: View {
    let gradientColors = Gradient(colors: [Color.pink, Color.purple])
    var body: some View {
        let linearGradient = LinearGradient(
            gradient: gradientColors,
            startPoint: .top, endPoint: .bottom)
        return Circle()
            .fill(linearGradient)
    }
}


そしてこれを複数組み合わせてViewを作ります。

GeometryReaderを使って
Viewの中に規則的にBallViewを配置しています。

https://developer.apple.com/documentation/swiftui/geometryreader

三角形をはみ出さないように
maskを使用してBallViewを描画する範囲を限定しています。
https://developer.apple.com/documentation/swiftui/view/3278595-mask


コード

struct BallsSlopeView<Mask: View>: View {
    let drawArea: Mask
    var body: some View {
        GeometryReader { gr in
            ForEach(1...10, id: \.self) { index in
                BallView()
                    .position(
                        self.getPosition(at: index,
                                         midX: gr.frame(in: .local).maxX,
                                         midY: gr.frame(in: .local).maxY))
                    .frame(height: 20)
            }
        }
        .mask(drawArea)
    }

    private func getPosition(at index: Int, midX: CGFloat, midY: CGFloat) -> CGPoint{
        let x = midX * CGFloat(1 - CGFloat(index) * 0.1)
        let y = midY * CGFloat(1 - CGFloat(index) * 0.05)
        return CGPoint(x: x, y: y)
    }
}


組み合わせる

最後に上記で作った部品を組み合わせます。

すべてをZStackでグループにして重ねます。
白い飾りの部分では
rotationEffectを活用することで
少し回転させて木にかかっているようにしています。

https://developer.apple.com/documentation/swiftui/scaledshape/3273943-rotationeffect


コード
struct TreeView: View {
    var body: some View {
        GeometryReader { gr in
            ZStack(alignment: .center) {
                Triangle()
                    .foregroundColor(Color.green)
                Slope()
                    .stroke(lineWidth: 20)
                    .mask(Triangle())
                    .foregroundColor(Color.white)
                Slope()
                    .stroke(lineWidth: 20)
                    .rotationEffect(Angle.degrees(300))
                    .mask(Triangle())
                    .foregroundColor(Color.white)
                BallsSlopeView(drawArea: Triangle())
            }
        }
    }
}


土台

スクリーンショット 2019-12-01 10.47.10.png

次に土台の部分を作ります。

Rectangleの中に白い線のShapeを載せます。

白い線はShapeで作成します。


コード
struct FoundationLine: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: 0, y: rect.maxY / 3))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY / 3))
            path.move(to: CGPoint(x: 0, y: rect.maxY * 2 / 3))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY * 2 / 3))
            path.move(to: CGPoint(x: rect.width / 3, y: 0))
            path.addLine(to: CGPoint(x: rect.width / 3, y: rect.maxY))
            path.move(to: CGPoint(x: rect.width * 2 / 3, y: 0))
            path.addLine(to: CGPoint(x: rect.width * 2 / 3, y: rect.maxY))
        }
    }
}


上記で作った白い線とRectangleZStackでグループにします。


コード
struct FoundationView: View {
    var body: some View {
        GeometryReader { gr in
            ZStack {
                Rectangle()
                    .foregroundColor(Color.red)
                FoundationLine()
                    .stroke(lineWidth: 3)
                    .foregroundColor(Color.white)
                    .mask(Rectangle())
            }
            .frame(width: gr.size.width / 3, height: gr.size.width / 3)
        }
    }
}


クリスマスツリーを組み立てる

ではこれまで作ったものを組み合わせます。
星と個々の木が少しづつ重なるように位置の調整をしています。

また
そのままですと
木の三角の重なり方が
逆になってしまう(上の頂点の部分が上に重なって見える)ため
zIndexで重なり方を変更しています。
https://developer.apple.com/documentation/swiftui/view/3278679-zindex


コード
struct ChristmasTree: View {
    var body: some View {
        GeometryReader { gr in
            VStack(spacing: -12) {
                VStack(spacing: -(gr.size.width * 0.1)) {
                    Star(corners: 5, smoothness: 0.5)
                        .foregroundColor(Color.yellow)
                        .frame(width: gr.size.width * 0.3,
                               height: gr.size.width * 0.3)
                        .zIndex(2)
                    ZStack {
                        VStack(spacing: -(gr.size.width / 5)) {
                            TreeView()
                                .frame(width: gr.size.width * 0.6)
                                .zIndex(3)
                            TreeView()
                                .frame(width: gr.size.width * 0.7)
                                .zIndex(2)
                            TreeView()
                                .frame(width: gr.size.width * 0.8)
                                .zIndex(1)
                        }
                        .frame(height: gr.size.height * 0.5)
                        .foregroundColor(Color.green)
                    }
                    .zIndex(1)
                }
                FoundationView()
                    .frame(height: gr.size.height * 0.2)
            }
        }
    }
}


背景

スクリーンショット 2019-12-01 10.50.50.png

次に背景を作成していきます。

Circleをランダムな大きさとopacityと位置に配置します。


コード
struct Particles: View {
    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
        }
    }
}


背景にアニメーションを設定する

せっかくなので背景にアニメーションをつけて
もう少し豪華(?)にしてみます。

今回はinteractiveSpringというAnimationを利用しました。


値は色々と触ってみて
こんな感じなのかなと思った値を設定しているので
適当です。

https://developer.apple.com/documentation/swiftui/animation/3344959-interactivespring

画面表示時にアニメーションを起こすための処理

SwiftUIのアニメーションを設定する上で注意したい点として
単純にアニメーションを設定しただけでは
アニメーションが起動しません。

これを画面表示時に発生させるためには
例えば@Stateを付けたの変数を
onAppearの中で変更することで
Viewの中の値を動的に変更させて再レンダリングさせるなどの
処理が必要になります。


コード
struct Particles: View {
    // レンダリングを起こすために必要
    @State private var scaling = false

    var animation: Animation {
        Animation
            .interactiveSpring(response: 5, dampingFraction: 0.5)
            .repeatForever()
            .speed(.random(in: 0.05...0.9))
            .delay(.random(in: 0...2))
    }

    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .animation(self.animation)
                        // この値を変化させることで再レンダリングを起こしている
                        .scaleEffect(self.scaling ? .random(in: 0.1...2) : 1)
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
            .onAppear {
                // レンダリングを起こすために必要
                self.scaling = true
            }
        }
    }
}


パフォーマンスを向上させる

画面としては上記で完成ですが
一つ問題があります。

上記のアニメーションの処理を行ったことによって
メモリの使用量がどんどん増えていきます。

drawNo_480.gif

※ この後もずっと増えていきます。

これは
ZStackの中の各Viewが描画をする際に
それぞれでレイヤーを構築します。

そうするとその分のメモリを使用する結果
CPUへの負荷大きくなります。

これはアプリのパフォーマンスの低下を招くことがあります。

そこでdrawingGroupを使って負荷を減らすことができます。

drawingGroup

https://developer.apple.com/documentation/swiftui/group/3284805-drawinggroup

このメソッドは
Viewの中の全てのViewを
画面上には見えないオフスクリーン上で
Metal APIを使用して
一つのイメージにまとめて描画し
最終的な内容を画面に出力するようにしてくれます。

こうすることでメモリへの負荷を軽減させて
パフォーマンスを向上させることができます。


コード
struct Particles: View {
    // レンダリングを起こすために必要
    @State private var scaling = false

    var animation: Animation {
        Animation
            .interactiveSpring(response: 5, dampingFraction: 0.5)
            .repeatForever()
            .speed(.random(in: 0.05...0.9))
            .delay(.random(in: 0...2))
    }

    var body: some View {
        GeometryReader { gr in
            ZStack {
                ForEach(0...200, id: \.self) { _ in
                    Circle()
                        .foregroundColor(.red)
                        .opacity(.random(in: 0.1...0.4))
                        .animation(self.animation)
                        // この値を変化させることで再レンダリングを起こしている
                        .scaleEffect(self.scaling ? .random(in: 0.1...2) : 1)
                        .frame(width: .random(in: 10...100),
                               height: .random(in: 10...100))
                        .position(x: .random(in: 0...gr.size.width),
                                  y: .random(in: 0...gr.size.height))
                }
            }
            // ここに設定をする
            .drawingGroup()
            .onAppear {
                // レンダリングを起こすために必要
                self.scaling = true
            }
        }
    }
}


draw_480.gif


一つ注意点として
drawingGroupのドキュメントに下記のような記載あります。

Views backed by native platform views don’t render into the image.

native platform viewsには
drawingGroupは効果がないようです。

このnative platform viewsとは
何を指すのかわからなかったのですが
twitter上でAppleの方が回答されていた内容によると
NSViewUIViewのことのようです。

https://twitter.com/jsh8080/status/1137045666939768833

まとめ

アドベントカレンダーのネタとして
クリスマスツリーを作っていく中で
SwiftUIの機能についていくつか見ていきました。

SwiftUIは宣言的に小さい部品を作り
それを組み合わせていくことができるので
再利用性が高く読みやすいなと改めて感じました。

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

APIKit による通信リトライの実装と、OHHTTPStubs によるその実装のテスト

これはゆめみ Advent Calendar 2019 3日目の記事です。

リトライ実装編

リクエストの再送信、つまりリトライはよくある用件ですが、通信のリトライの一番面倒なところは通信は基本非同期で動くので、初心者にはかなり厄介な部分になるかと思います。しかし実はこの非同期のリトライ要件は、再帰呼び出しとクロージャの組み合わせを使えばそれほど難しい要件ではありません。

今リファクタリングにあたってるとあるプロジェクトでは通信部分を APIKit で実装しているので、今日はこの APIKit を利用した場合のリトライ実装を解説します。

まず、リトライの実現するための再帰呼び出しですが、私の経験則では 外に使ってもらうための本呼び出し と、本呼び出しないで実際使っている再帰用呼び出し を分けて作った方が仕様変更に柔軟なのと、ソースコードが読みやすいです。つまり:

SomeObject
// 再帰呼び出し用のメソッド
private func recursiveDoSomething() {

    recursiveDoSomething()

}

// 外側に使ってもらうためのメソッド
func doSomething() {

    recurciveDoSomething()

}
Body
doSomething()

という感じです。確かに今のこの実装では doSomethingrecursiveDoSomething も中身は一緒ですが、しかし実際この二つのメソッドは二つの異なる要件を満たすためにあります。前者は単純に「リクエストを投げる」ためにあるのですが、後者は「万が一通信が失敗した時に、もう一回同じリクエストを投げる」ためにあるのです。用件が違うので、当然それぞれ細かい動作が微妙に違うことも予想できます。DRY 原則でも書いてありますように、重複してはいけないのはソースコードではなく知識=コンテキストです。むしろコンテキストが違えば、たまたま同じコードになってるとしてもそれは分けるべきです。

ちょっと脱線しすぎちゃいましたので話戻します。そのリトライをする際に当然ながら回数の制限をしなくてはいけません、じゃないと通信不良が続くといつまで経っても処理が終わらないので無限ループになります。というわけでここでは一旦デフォルト最大 5 回までリトライする、という要件を設定しておきます。

さて仕様を整理します:

  1. 特定なリクエストを投げて、レスポンスもらったら終了ハンドラーを実行します
  2. 通信不良によるタイムアウト等のレスポンスをもらった場合、もう一回1.を繰り返します
  3. 最大5回まで2.を実行しますが、それでもダメなら終了ハンドラーにエラーレスポンスを入れて実行します。

ここまでわかったらコード化するのは簡単ですね:

APIClient
// 再帰呼び出し用のメソッド
// `Request` 及び `Request.Response` は APIKit によって定義されたリクエストとレスポンスのprotocol
private func recursiveSend(_ request: Request, availableRetryTimes: Int, completionHandler: @escaping (Result<Request.Response, Error>) -> Void) {

    // `session` は APIKit によって定義されたリクエストを投げるためのセッション
    session.send(request) { result in
        switch result {
        case .success(let response):
            completion(.success(response)

        case .failure(let error)
            // 今回はサンプルコードの単純化のために全てのエラーレスポンスを再帰呼び出ししていますが、実際はエラーレスポンスの種類によって切り替えています;
            // 例えばそもそもリクエスト自体に問題があった場合は何度リトライしても同じ結果なのでリトライしない
            if availableRetryTimes > 0 {
                recursiveSend(request, availableRetryTimes: availableRetryTimes - 1, completionHandler: completionHandler)
            } else {
                completion(.failure(error))
            }
        }
    }

}

func send(_ request: Request, maxRetryTimes: Int = 5, completion: @escaping (Result<Request.Response, Error>) -> Void) {

    recursiveSend(request, availableRetryTimes: maxRetryTimes, completionHandler: completion)

}

// 下記の `request` は実際の API にアクセスするための APIKit のリクエストインスタンス
send(request) { result in
    // doSomething(with: result)
}

お気づきでしょうか、リトライ回数の制御に、私は敢えてそれぞれ違う引数名にしています。外側の呼び出し用のものは「最大リトライ回数」の名前にしていますが、再帰メソッドでは「リトライ可能回数」にしているのです。それぞれ違うコンテキストになりますので。そしてこの二つのメソッドを分けることによって、最大リトライ回数の引数のデフォルト値を設定しながら、再帰呼び出しの関数には該当引数を必ず入れなくてはならない安全性も担保できています。

サンプル記事としての簡潔のために、エラー処理を大幅に簡略化していますが、実際はコメントに書いてあるとおりエラーによって切り分けが必要です。この記事ではあくまでリトライの実現に関する記事なのでとりあえず全てのエラーをリトライさせています。そしてリトライ処理の肝がこれだけです:

APIClient
            if availableRetryTimes > 0 {
                recursiveSend(request, availableRetryTimes: availableRetryTimes - 1, completionHandler: completionHandler)
            } else {
                completion(.failure(error))
            }

ご覧の通り、もしエラーがもらったら、まずリトライ可能の回数を確認します。もしもうリトライ可能回数がまだある(0 以上)なら、リトライ可能回数を 1 つ減らして、それ以外の引数は全てそのまま使ってこの再帰呼び出しメソッドを呼び出します;もしもうリトライ可能回数がもうない(0 かそれ以下)になったら、終了ハンドラーに今のエラーを渡して実行させます。再帰呼び出されるときは必ずリトライ可能回数が減るので、度重なる先呼び出しでいつかは 0 になって終了ハンドラーが呼び出されます。

ね、簡単でしょ?

テスト実装編

さて、リトライの実装ができたら、今度はこのリトライのロジックをテストしなくてはならないですね。リトライのテストですから、当然テスト対象として:

  1. 1 回目で成功レスポンスもらったらそのまま終了ハンドラーを回す
  2. 1 回目で成功レスポンスもらえず、ただしリトライ可能な失敗レスポンスもらったら所定の回数まで回して、途中で成功レスポンスもらえたらそのまま終了ハンドラーを回す
  3. 所定の回数まで回して全部失敗レスポンスなら終了して終了ハンドラーに失敗レスポンスを入れて回す
  4. 1回目で成功レスポンスでもリトライ可能な失敗レスポンスでもなく、リトライ不可な失敗レスポンスもらったらそのまま終了ハンドラーに失敗レスポンス入れて回す

といった感じですかね。

しかしテストするときに必ずしもネットワークが悪いとは限らないし、ましては CI 環境ならなおさらのことです。ここで役に立つのは特定な通信に独自のレスポンスを返させるスタブライブラリーです。この記事では弊社でもよく利用している OHHTTPStubs を使って解説します。

OHHTTPStubs を使ってスタブを作るのはとても簡単です。通信が開始する前に、このように設定してあげればいいです:

Test
stub(condition: pathEndsWith("/path/of/api")) { _ in
    let responseObject: [String: String] = ["key": "value"] // 成功レスポンス
    return OHHTTPStubsResponse(jsonObject: responseObject, statusCode: 200, headers: nil)
}

上記のように設定すれば、/path/of/api で終わるパスにアクセスするときに、自分で設定した responseObject が返されます。

しかし、これでは何回 /path/of/api アクセスしても同じ成功レスポンスが返されますが、我々が欲しいのは特定の回数にアクセスした時だけ成功し、それ以外では失敗するレスポンスが欲しいです。どうしましょう?

この問題は実は簡単です。OHHTTPStubs の具体的になんのレスポンスを返すかの処理は、静的なレスポンスオブジェクトではなく、動的にレスポンスオブジェクトを生成するクロージャなので、アクセスする回数のカウンターをつけてあげればいいです。そのカウンター変数は XCTestCase クラスにプロパティーとしてつけてあげるのもいいですが、ここで紹介したいのはプロパティーを作らずに通常の変数としてキャプチャーさせる方法です。この方法は本呼び出しの stub(condition:response) メソッドをラップしたメソッドを作り、このラップメソッドの中に変数を組み込んで response クロージャにキャプチャーさせればいいです。つまりコードにするとこんな感じです:

Test
private func setStub(condition: @escaping OHHTTPStubsTestBlock, successOnTimeOfRequestReceived: Int, response: @escaping OHHTTPStubsResponseBlock) {

    var timesOfRequestReceived = 0

    stub(condition: condition) { (request) -> OHHTTPStubsResponse in
        timesOfRequestReceived += 1
        if timesOfRequestReceived == successOnTimesOfRequestReceived {
            return response(request)

        } else {
            return OHHTTPStubsResponse(error: NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue))
        }
    }

}


setStub(condition: pathEndsWith("/path/of/api"), successOnTimeOfRequestReceived: 1) { _ in
    let responseObject: [String: String] = ["key": "value"] // 成功レスポンス
    return OHHTTPStubsResponse(jsonObject: responseObject, statusCode: 200, headers: nil)
}

このように setStub を呼び出せば、中の処理として、(request) -> OHHTTPStubsResponse クロージャは該当パスにアクセスされるたびに実行されるので、実行したらラッパーメソッドの timesOfRequestReceived を取得し +=1 して現在のアクセス回数を割り出します。初期値が 0 で必ず値をプラス 1 してから評価するので、1 回目のアクセスは timesOfRequestReceived を評価するときの値が 1 になります。そしてこの timesOfRequestReceivedsuccessOnTimesOfRequestReceived と同じ値になる時のみ、成功レスポンスを返し、それ以外の時は URLError.notConnectedToInternet のエラーが返されるようにスタブが設定されます。

ちなみにこのようにラッパーメソッド内にクロージャで使う変数を作るメリットは、このラッパーメソッドにとってスレッドセーフになるのと、大元のオブジェクトに余計な状態を持さず、プロパティーを汚さないで済むことです。

ここまでわかったら、後のテストケースの作成は簡単ですね、こんな感じにすればいいです:

SessionTaskError
// `APIKit.SessionTaskError` は `Equatable` に適合していないので、ひとまず対応させておく
extension APIKit.SessionTaskError: Equatable {

    public static func == (lhs: AppError.External, rhs: AppError.External) -> Bool {
        return lhs.localizedDescription == rhs.localizedDescription
    }

}
Test
// ここの `Response` も実際の `Request.Response` に置き換えられますが、これも `XCTAssertEqual` を使うために `Equatable` に適合する必要があります
// `expectedResponse` が実際の成功レスポンスインスタンス
typealias TestCase = (successOnTime: Int, expectedResult: Result<Response, APIKit.SessionTaskError>)
let testCases: [TestCase] = [
    (0, .failure(.connectionError(NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue)))),
    (1, .success(expectedResponse)),
    (2, .success(expectedResponse)),
    (3, .success(expectedResponse)),
    (4, .success(expectedResponse)),
    (5, .success(expectedResponse)),
    (6, .success(expectedResponse)),
    (7, .failure(.connectionError(NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue)))),
]

for testCase in testCases {

    let testExpectation = expectation(description: "\(testCase)")
    useStub(successOnTimeOfRequestReceived: testCase.successOnTime)
    send(request) { (result) in // この `request` は実際の API パス `/path/of/api` にアクセスするための APIKit のリクエストインスタンス
        defer { 
            OHHTTPStubs.removeAllStubs()
            testExpectation.fulfill()
        }

        XCTAssertEqual(result, testCase.expectedResult)
    }
    wait(for: [testExpectation], timeout: 5)
}

このように、成功レスポンスを返すときのアクセス回数と、想定されているレスポンスのタップルのテストケース配列を作っておけば、あとはこの配列を回して、実際に本当に想定通りにリトライしているのかが確認できます。

ちなみにここで注意しないといけないのは、上のリトライ実装編で定義したリトライ回数のデフォルト値が 5 ですが、これはリトライ回数ですので、最初のアクセスの試みも含めれば合計最大 6 回のアクセスが可能となる仕様になります。この人によってわかりにくいかもしれない仕様も、きちんとテストすることで保証されることがわかります。


最後に、リトライ実装編ではこの解説コードをシンプルにするために、リトライすべき失敗レスポンスとすべきでない失敗レスポンスを区別させていないため、ここのテストも最後のテスト対象である

 4. 1回目で成功レスポンスでもリトライ可能な失敗レスポンスでもなく、リトライ不可な失敗レスポンスもらったらそのまま終了ハンドラーに失敗レスポンス入れて回す

をテストしていません。しかしこの記事では一番肝なアプローチを解説しましたので、この仕様の対応はもう難しくないはずです。興味ある方は是非この失敗レスポンスの切り分けとテストの実装を試してみてください。

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

去年の自分にマサカリを投げる

まえがき

こんにちはrenです。
世間は12月。すっかりアドカレの季節ですね。

アドカレといえば去年のアドカレではこんな記事を書きました。
iOSアプリでよく見るチュートリアル画面を作成する

え、なにこれ。
読みづらすぎる。

ということで、去年の自分にマサカリを投げることにしました。

本編

まず、前回のコードを見返してみましょう。

ViewController.swift

// ViewController.swift
import UIKit

class ViewController: UIViewController{

    var tutorialCollectionView: UICollectionView!
    var layout: UICollectionViewFlowLayout!

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトを生成
        layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: viewWidth, height: viewHeight)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        // CollectionViewを生成
        tutorialCollectionView = UICollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // Cellに使われるクラスを登録
        tutorialCollectionView.register(CustomUICollectionViewCell.self, forCellWithReuseIdentifier: "CustomCell")

        // dataSourceを自身に設定
        tutorialCollectionView.dataSource = self

        // ページングさせる
        tutorialCollectionView.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        tutorialCollectionView.showsHorizontalScrollIndicator = false

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    override func viewDidAppear(_ animated: Bool) {
        // ViewDidLoadではSafeAreaが取得できないのでここでリサイズ
        let safeArea = self.view.safeAreaInsets
        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height
        let collectionViewFrame = CGRect (x: safeArea.left, y: safeArea.top, width: viewWidth - safeArea.left, height: viewHeight - safeArea.top - safeArea.bottom)

        layout.itemSize = CGSize(width: viewWidth - safeArea.left, height: viewHeight - safeArea.top - safeArea.bottom)

        tutorialCollectionView.frame = collectionViewFrame

    }

}

extension ViewController: UICollectionViewDataSource {

    // Cellの数を設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            cell.backgroundColor = UIColor.blue
        case 1:
            cell.backgroundColor = UIColor.orange
        case 2:
            cell.backgroundColor = UIColor.yellow
        case 3:
            cell.backgroundColor = UIColor.green
        case 4:
            cell.backgroundColor = UIColor.red
        default:
            break
        }

        cell.textLabel?.text = "\(indexPath.row + 1)ページ目"

        return cell
    }

}

CustomUICollectionViewCell.swift

// CustomUICollectionViewCell
import UIKit

class CustomUICollectionViewCell : UICollectionViewCell{

    var textLabel : UILabel?

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        // UILabelを生成.
        textLabel = UILabel(frame: CGRect(x:0, y:0, width:frame.width, height:frame.height))
        textLabel?.text = "nil"
        textLabel?.textAlignment = NSTextAlignment.center

        // Cellに追加.
        self.contentView.addSubview(textLabel!)
    }

}

うん。読みづらい。
どんなところが読みづらいのか、説明していきます。

viewDidLoadに処理書きすぎ

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトを生成
        layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: viewWidth, height: viewHeight)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        // CollectionViewを生成
        tutorialCollectionView = UICollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // Cellに使われるクラスを登録
        tutorialCollectionView.register(CustomUICollectionViewCell.self, forCellWithReuseIdentifier: "CustomCell")

        // dataSourceを自身に設定
        tutorialCollectionView.dataSource = self

        // ページングさせる
        tutorialCollectionView.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        tutorialCollectionView.showsHorizontalScrollIndicator = false

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

処理を分割してみましょう

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // CollectionViewを生成
        tutorialCollectionView = UICollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewを設定
        setupTutorialCollectionView()

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

    func setupTutorialCollectionView() {
        // Cellに使われるクラスを登録
        tutorialCollectionView.register(CustomUICollectionViewCell.self, forCellWithReuseIdentifier: "CustomCell")

        // dataSourceを自身に設定
        tutorialCollectionView.dataSource = self

        // ページングさせる
        tutorialCollectionView.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        tutorialCollectionView.showsHorizontalScrollIndicator = false
    }

だいぶマシにはなりましたね。

UICollectionViewDataSource を直接 ViewController に準拠させている

extension ViewController: UICollectionViewDataSource {

    // Cellの数を設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            cell.backgroundColor = UIColor.blue
        case 1:
            cell.backgroundColor = UIColor.orange
        case 2:
            cell.backgroundColor = UIColor.yellow
        case 3:
            cell.backgroundColor = UIColor.green
        case 4:
            cell.backgroundColor = UIColor.red
        default:
            break
        }

        cell.textLabel?.text = "\(indexPath.row + 1)ページ目"

        return cell
    }

}

これでは ViewController の仕事が増えすぎてしまいます。

こういう時は UICollectionView のカスタムクラスを作りましょう。

import UIKit

class TutorialCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension TutorialCollectionView: UICollectionViewDataSource {
    // Cellの数を設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            cell.backgroundColor = UIColor.blue
        case 1:
            cell.backgroundColor = UIColor.orange
        case 2:
            cell.backgroundColor = UIColor.yellow
        case 3:
            cell.backgroundColor = UIColor.green
        case 4:
            cell.backgroundColor = UIColor.red
        default:
            break
        }

        cell.textLabel?.text = "\(indexPath.row + 1)ページ目"

        return cell
    }

}

ここに先程viewDidLoad から抜き出した setupTutorialCollectionView を移動させます。

import UIKit

class TutorialCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setup() {
        // Cellに使われるクラスを登録
        tutorialCollectionView.register(CustomUICollectionViewCell.self, forCellWithReuseIdentifier: "CustomCell")

        // dataSourceを自身に設定
        self.dataSource = self

        // ページングさせる
        self.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        self.showsHorizontalScrollIndicator = false

        // BackgroundColorを白にする。
        self.backgroundColor = .white
    }
}

extension TutorialCollectionView: UICollectionViewDataSource {
    // Cellの数を設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    // Cellに値を設定する
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            cell.backgroundColor = UIColor.blue
        case 1:
            cell.backgroundColor = UIColor.orange
        case 2:
            cell.backgroundColor = UIColor.yellow
        case 3:
            cell.backgroundColor = UIColor.green
        case 4:
            cell.backgroundColor = UIColor.red
        default:
            break
        }

        cell.textLabel?.text = "\(indexPath.row + 1)ページ目"

        return cell
    }

}

UICollectionViewTutorialCollectionView に変更

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // TutorialCollectionViewを生成
        tutorialCollectionView = TutorialCollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

cellForItemAtCell の設定をしている

    // Cellに値を設定する
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            cell.backgroundColor = UIColor.blue
        case 1:
            cell.backgroundColor = UIColor.orange
        case 2:
            cell.backgroundColor = UIColor.yellow
        case 3:
            cell.backgroundColor = UIColor.green
        case 4:
            cell.backgroundColor = UIColor.red
        default:
            break
        }

        cell.textLabel?.text = "\(indexPath.row + 1)ページ目"

        return cell
    }

せっかくカスタムセルを作っているので、セルの設定もセル側にやってもらったほうが処理がスッキリします。

import UIKit

class CustomUICollectionViewCell : UICollectionViewCell{

    var textLabel : UILabel?

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        // UILabelを生成.
        textLabel = UILabel(frame: CGRect(x:0, y:0, width:frame.width, height:frame.height))
        textLabel?.text = "nil"
        textLabel?.textAlignment = NSTextAlignment.center

        // Cellに追加.
        self.contentView.addSubview(textLabel!)
    }

    func setCell(indexPath: IndexPath) {
        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            self.backgroundColor = UIColor.blue
        case 1:
            self.backgroundColor = UIColor.orange
        case 2:
            self.backgroundColor = UIColor.yellow
        case 3:
            self.backgroundColor = UIColor.green
        case 4:
            self.backgroundColor = UIColor.red
        default:
            break
        }

        pageNumberLabel.text = "\(indexPath.row + 1)ページ目"
    }

}
    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell: CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as! CustomUICollectionViewCell

        cell.setCell(indexPath: indexPath)

        return cell
    }

細かいところですが、必要でないのならOptionalは使わないようにしましょう。

import UIKit

class CustomUICollectionViewCell : UICollectionViewCell{

    var textLabel = UILabel() // Optionalでなくてよい

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        // UILabelを生成.
        textLabel = UILabel(frame: CGRect(x:0, y:0, width:frame.width, height:frame.height))
        textLabel.text = "nil"
        textLabel.textAlignment = NSTextAlignment.center

        // Cellに追加.
        self.contentView.addSubview(textLabel)
    }

    func setCell(indexPath: IndexPath) {
        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            self.backgroundColor = UIColor.blue
        case 1:
            self.backgroundColor = UIColor.orange
        case 2:
            self.backgroundColor = UIColor.yellow
        case 3:
            self.backgroundColor = UIColor.green
        case 4:
            self.backgroundColor = UIColor.red
        default:
            break
        }

        pageNumberLabel.text = "\(indexPath.row + 1)ページ目"
    }

}

強制キャスト as! もできるだけ使わないほうが安全です。

    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        guard let cell: CustomUICollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath as IndexPath) as? CustomUICollectionViewCell else { return UICollectionViewCell() }

        cell.setCell(indexPath: indexPath)

        return cell
    }

CustomUICollectionViewCell では役割が分かりづらい

TutorialCollectionViewCell に変更しましょう。

import UIKit

class TutorialCollectionViewCell: UICollectionViewCell {

    ・・・

}
    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TutorialCollectionViewCell", for: indexPath as IndexPath) as? TutorialCollectionViewCell else { return UICollectionViewCell() }

        cell.setCell(indexPath: indexPath)

        return cell
    }

identifierがハードコーディングになっているので、定数にしましょう。

import UIKit

class TutorialCollectionViewCell: UICollectionViewCell {

    public static let identifier = "TutorialCollectionViewCell"
    var textLabel = UILabel()

    ・・・
}
class TutorialCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setup() {
        // Cellに使われるクラスを登録
        tutorialCollectionView.register(TutorialCollectionViewCell.self, forCellWithReuseIdentifier: TutorialCollectionViewCell.identifier)

        // dataSourceを自身に設定
        self.dataSource = self

        // ページングさせる
        self.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        self.showsHorizontalScrollIndicator = false

        // BackgroundColorを白にする。
        self.backgroundColor = .white
    }
}
    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TutorialCollectionViewCell.identifier, for: indexPath as IndexPath) as? TutorialCollectionViewCell else { return UICollectionViewCell() }

        cell.setCell(indexPath: indexPath)

        return cell
    }

viewDidAppear でリサイズしている

現在のViewController を確認します。

viewDidLoadで初期化したものを viewDidAppear でリサイズしていますね。

この書き方は冗長に思えます。

import UIKit

class ViewController: UIViewController {

    var tutorialCollectionView: UICollectionView!
    var layout: UICollectionViewFlowLayout!

    override func viewDidLoad() {
        super.viewDidLoad()

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // TutorialCollectionViewを生成
        tutorialCollectionView = TutorialCollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    override func viewDidAppear(_ animated: Bool) {
        // ViewDidLoadではSafeAreaが取得できないのでここでリサイズ
        let safeArea = self.view.safeAreaInsets
        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height
        let collectionViewFrame = CGRect (x: safeArea.left, y: safeArea.top, width: viewWidth - safeArea.left, height: viewHeight - safeArea.top - safeArea.bottom)

        layout.itemSize = CGSize(width: viewWidth - safeArea.left, height: viewHeight - safeArea.top - safeArea.bottom)

        tutorialCollectionView.frame = collectionViewFrame

    }

    func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

}

viewWillLayoutSubviews で初期化すればリサイズをする必要場なくなります。

import UIKit

class ViewController: UIViewController {

    var tutorialCollectionView: UICollectionView!
    var layout: UICollectionViewFlowLayout!

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

    override func viewWillLayoutSubviews() {

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // TutorialCollectionViewを生成
        tutorialCollectionView = TutorialCollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

}

暗黙的アンラップ型 ! はあまり使わないようにしましょう。

import UIKit

class ViewController: UIViewController {

    lazy var tutorialCollectionView = TutorialCollectionView()
    var layout = UICollectionViewFlowLayout()

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

    override func viewWillLayoutSubviews() {

        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: 0, y: 0, width: viewWidth, height: viewHeight)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // TutorialCollectionViewを生成
        tutorialCollectionView = TutorialCollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)

    }

    func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

}

最後にアクセス修飾子を付けたら完成です。


リファクタ後

ViewController

import UIKit

class ViewController: UIViewController {

    private lazy var tutorialCollectionView = TutorialCollectionView()
    private var layout = UICollectionViewFlowLayout()

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

    override func viewWillLayoutSubviews() {
        setTutorialCollectionView()
    }

    private func setTutorialCollectionView() {
        let safeArea = self.view.safeAreaInsets
        let viewWidth = self.view.frame.width
        let viewHeight = self.view.frame.height

        let collectionViewFrame = CGRect (x: safeArea.left,
                                          y: safeArea.top,
                                          width: viewWidth - safeArea.left - safeArea.right,
                                          height: viewHeight - safeArea.top - safeArea.bottom)

        // CollectionViewのレイアウトをセット
        layout = setLayout(width: viewWidth, height: viewHeight)

        // CollectionViewを生成
        tutorialCollectionView = TutorialCollectionView(frame: collectionViewFrame, collectionViewLayout: layout)

        // CollectionViewをViewに追加する
        self.view.addSubview(tutorialCollectionView)
    }

    private func setLayout(width: CGFloat, height: CGFloat) -> UICollectionViewFlowLayout {
        // CollectionViewのレイアウトを生成
        let layout = UICollectionViewFlowLayout()

        // Cell一つ一つの大きさを設定
        layout.itemSize = CGSize(width: width, height: height)

        // Cellの行間隔を設定
        layout.minimumLineSpacing = 0

        // Cellの列間隔を設定
        layout.minimumInteritemSpacing = 0

        // CollectionViewのスクロールの方向を横にする
        layout.scrollDirection = .horizontal

        return layout
    }

}

TutorialCollectionView

import UIKit

class TutorialCollectionView: UICollectionView {

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {
        // Cellに使われるクラスを登録
        self.register(TutorialCollectionViewCell.self, forCellWithReuseIdentifier: TutorialCollectionViewCell.identifier)

        // dataSourceを自身に設定
        self.dataSource = self

        // ページングさせる
        self.isPagingEnabled = true

        // ScrollIndicatorを非表示にする
        self.showsHorizontalScrollIndicator = false

        // BackgroundColorを白にする。
        self.backgroundColor = .white
    }
}

extension TutorialCollectionView: UICollectionViewDataSource {
    // Cellの数を設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }

    // Cellに値を設定する
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // Cellを取得
        guard let cell: TutorialCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: TutorialCollectionViewCell.identifier, for: indexPath as IndexPath) as? TutorialCollectionViewCell else { return UICollectionViewCell() }
        // Cellに値を設定する
        cell.setCell(indexPath: indexPath)

        return cell
    }

}

TutorialCollectionViewCell

import UIKit

class TutorialCollectionViewCell: UICollectionViewCell {

    public static let identifier = "TutorialCollectionViewCell"

    private var pageNumberLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        addLabel()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func addLabel() {
        // UILabelを生成.
        pageNumberLabel = UILabel(frame: CGRect(x:0, y:0, width:frame.width, height:frame.height))
        pageNumberLabel.text = ""
        pageNumberLabel.textAlignment = NSTextAlignment.center

        // Cellに追加.
        self.contentView.addSubview(pageNumberLabel)
    }

    public func setCell(indexPath: IndexPath) {
        // Cellに応じてbackgroundColorを変更
        switch indexPath.row {
        case 0:
            self.backgroundColor = UIColor.blue
        case 1:
            self.backgroundColor = UIColor.orange
        case 2:
            self.backgroundColor = UIColor.yellow
        case 3:
            self.backgroundColor = UIColor.green
        case 4:
            self.backgroundColor = UIColor.red
        default:
            break
        }

        pageNumberLabel.text = "\(indexPath.row + 1)ページ目"
    }

}

ソースコードはGitHubにあげました
https://github.com/renchild8/TutorialCollectionView

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

[Cocoa][Swift]XCFramework

Darwinで採用されています実行形式のバイナリ・フォーマットMach-oは、一つのファイルに複数のアーキテクチャのバイナリが格納できるという素晴らしい特徴があるのですが、同じCPUで異なるシステム向けのバイナリは同時に格納できないという欠点があるようです。以前だと、これで問題はなかったのですが、例えば、iPad OS向けアプリのソースからmacOSアプリを作ることができるUIKit for Mac (Catalyst)だと、x86_64でiOSとiPhoneシミュレータ(macOS)という場合が発生して、同一ファイルに格納できないという問題が発生します。

おそらく、これの対策として用意されたのが、Xcode 11から利用できるXCFramework。簡単に説明すると複数のフレームワークを一つにできるというものだ。

具体的には、MoltenGLというライブラリはiOSとmacOS向けのフレームワークが用意されていて、これを以下のコマンドでXCFrameworkにまとめられる。

% xcodebuild -create-xcframework \
> -framework MoltenGL-0.25.0/MoltenGL/iOS/framework/MoltenGL.framework \
> -framework MoltenGL-0.25.0/MoltenGL/macOS/framework/MoltenGL.framework \
> -output MoltenGL.xcframework
xcframework successfully written out to: MoltenGL.xcframework

この中身をtreeコマンドで確認してみる。

% tree MoltenGL.xcframework
MoltenGL.xcframework
├── Info.plist
├── ios-armv7_arm64
│   └── MoltenGL.framework
│       ├── Headers
│       │   ├── MoltenGL.h
│       │   ├── mglDataTypes.h
│       │   ├── mglEnv.h
│       │   ├── mglGLKitDataTypes.h
│       │   ├── mglMetalState.h
│       │   ├── mglext.h
│       │   └── mln_env.h
│       └── MoltenGL
└── macos-x86_64
    └── MoltenGL.framework
        ├── Headers -> Versions/Current/Headers
        ├── MoltenGL -> Versions/Current/MoltenGL
        └── Versions
            ├── A
            │   ├── Headers
            │   │   ├── MoltenGL.h
            │   │   ├── mglDataTypes.h
            │   │   ├── mglEnv.h
            │   │   ├── mglGLKitDataTypes.h
            │   │   ├── mglMetalState.h
            │   │   ├── mglext.h
            │   │   └── mln_env.h
            │   └── MoltenGL
            └── Current -> A
 
10 directories, 18 files

単なるx86_64でなく、macOSのx86_64となっている。

【関連情報】
Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

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

Alamofireを使用したAPI通信について(MVC)

初めまして!!
休学をしてプログラミングを学んでいるりゅーちゃんです!!
今回はアドベントカレンダーの3日目を担当します!:bow_tone1:
題名にも記載しているように今回はAlamofireについて解説していきます:clap:
(自分自身まだ初学者なのでどんどんFBしてくださると助かります!!ただ優しくしていただけると幸いです:pray:

Alamofireについて:relaxed:(今回の記事の概要)

言わずともしれたAPI通信を行う際の優秀なライブラリです!!

(APIKitとは何が違うのか?については知識不足でまだわかっていません:cold_sweat:
Qiitaの記事も豊富で学習がしやすく、ドキュメントを参照すればすぐに使うことができ初心者にも優しいものです!!:joy:

ではなぜ今回は改めて記事を書う理由は。。。
  • MVCを採用した際のデーターの渡し方の説明が少ない記事が多い(Alamofireの使い方に重点をおいているため)
  • 大まかな情報の流れについて記事が難しすぎる
  • 自分自身の振り返りのため

この3つの要因があります!!
稚拙な文章ですが最後までお付き合いしてくださると幸いです!!:muscle:

MVCを利用したAPI通信について(Alamofireを添えて:wine_glass:

まずは行いたい処理を確認していきます!!:writing_hand:
1. ViewControllerから通信に必要な情報をModelに渡してあげる:eyes:
2. ModelでViewControllerから受け取った情報をheadersに詰め込みAPI通信を行う
3. ViewControllerに取得データを出力する(本来ならTableViewなどに表示するが今回は割愛します:disappointed_relieved:
大まかに分けてこの3つの処理を実行していきます!!

Sample.json
{
"data": [
        {
            "name": "牛肉",
            "food_type": "meat",
            "rate": 60,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "豚肉",
            "food_type": "meat",
            "rate": 30,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "鶏肉",
            "food_type": "meat",
            "rate": 0,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "鯖",
            "food_type": "fish",
            "rate": 0,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "イワシ",
            "food_type": "fish",
            "rate": 0,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "にんじん",
            "food_type": "vegetable",
            "rate": 30,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "キャベツ",
            "food_type": "vegetable",
            "rate": 100,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "玉ねぎ",
            "food_type": "vegetable",
            "rate": 100,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "じゃがいも",
            "food_type": "vegetable",
            "rate": 100,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        },
        {
            "name": "もやし",
            "food_type": "vegetable",
            "rate": 100,
            "user": {
                "uuid": "2553846F-996A-4D3F-93FC-9ACD8D8CD16B"
            }
        }
    ]
}

このJsonデーターを取得しViewで表示するのが今回のゴールです!!

まずはAPI通信のした準備としてCodableを見ていきましょう:thinking:

SampleResponse.swift
struct MetaFoodTypes: Codable {
    let data: [FoodTypes]
}

struct FoodTypes: Codable {
    let name: String
    let foodType: String
    var rate: Int

//スネークケースをキャメルケースに変換している
    enum CodingKeys: String, CodingKey {
        case name
        case foodType = "food_type"
        case rate
    }
}

今回はJSONの形からdataをKeyとしてその中の情報を取得する必要があります!!
そこで上記のようにMetaFoodTypes、FoodTypesを作ることCodableがMetaFoodTypesを見てくれた後にFoodTypesを見る処理を書いています:relaxed:

https://qiita.com/_ha1f/items/bf1aad5ea3e927f59f9d
今回の説明では不十分だと感じた方はこの方の記事を見るのをお勧めします:punch:

次はViewControllweです!!

ViewController.swift
private let model = ViewModel()

private func getFoodTypesdata() {
//userdefaultsの処理については後ほど記述します!!
//tokenを取得しModelに情報を渡してあげる
   guard let token = UserData.token else { return }
        model.getFoodtypes(token: token, handler: { result in
//Modelからの処理の結果がresult部分に渡される
                     print(result)
     })
}

追記:userdefaultsについて

UserDefault.swift
struct UserData {
    static let userDefault = Foundation.UserDefaults.standard

    struct Key {
        static let token = "token"
    }
}

extension UserData {
    static var token: String? {
        get {
            return userDefault.string(forKey: Key.token) ?? nil
        } set {
            userDefault.set(newValue, forKey: Key.token)
            userDefault.synchronize()
        }
    }
}

ここの処理についてはまだまだ言語化ができていないため、理解次第この記事を編集していきます:bow_tone2:
(参照した記事がQiitaではないため今回は参照リンクは記載しません。。。)

最後にViewModelを見ていきましょう!!
ViewModel.swift
import Foundation
import Alamofire

class SelectViewModel {
 func getFoodTypesdata (token: String, handler: @escaping (MetaFoodTypes) -> Void) {

        let url = URL(string: "********************")!

        //ここでViewから取得してきた情報を使用している
        let headers: HTTPHeaders = [
            "**********": "********" + token
        ]

        AF.request(url, method: .get, encoding: JSONEncoding.default, headers: headers)
            .validate(statusCode: 200..<300)
            .responseJSON{ response in
                switch response.result {
                case .success( _):
                    guard let data = response.data else { return }
                    guard let foodTypes = try? JSONDecoder().decode(MetaFoodTypes.self, from: data) else { return }
             //ここの処理の結果がViewのhandlerに格納される!!
                    handler(foodTypes)
                case .failure(let error):
                    print(error)
                }
        }
    }
}

この部分の処理についてはクロージャを使っていること以外はAlamofireのドキュメントを読むことで理解することができました!!
是非とも英語ばかりで読みたくはない(実際に自分がそうでした:sweat_smile:)と思いますが理解度が深くなるので是非ともチャレンジしてください!!

まとめ

今回は大まかに情報の流れに焦点を当てて記事を書いてみました!!
まだまだ1つ、1つの処理に関しては理解が足りない部分が多いですが勉強を怠らず、強くなっていきたいです:muscle:
間違っているぞ!!、ここはもっと詳しく書いてくれ!!などがありましたら気軽にコメント、編集リクエストしてください:pray:
今後の課題としてはAPI通信部分の共通化について取り組んでいきます!!!
(参考にできるものや、書き方を教えてくださると泣いて喜びます:joy:

またこんな初学者が諦めずに頑張れているのはTechTrainというサービスを利用し、壁にぶつかるたびにメンターさんに相談することができたからです:angel:
詳しい概要はTechTrain Advent Calendar 2019 1日目に記載してありますので気になった方はみてみてください!!:wave:

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