20200113のSwiftに関する記事は14件です。

#19 AdMobを使う場合のアプリの申請1例

はじめに

個人のメモ程度の出来なのであまり参考にしないで下さい.

環境

Xcode:11.2.1
Swift:5.1.2
2019/11

part1

全てはいを選択する.

スクリーンショット 2020-01-13 午後11.11.24.png

スクリーンショット 2020-01-13 午後11.11.37.png

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

#18 ホーム画面に表示するアプリ名を変更する1例

はじめに

個人のメモ程度の出来なのであまり参考にしないで下さい.

環境

Xcode:11.2.1
Swift:5.1.2
2019/11

part1

Info.plisetBundle display nameを任意の文字列に変更する.
スクリーンショット 2020-01-13 午後9.55.44.png

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

テストのためのDI(Dependency Injection)パターンを考える

k2moons@BlueEventHorizonです。昨年から気になっていたテストとDIについてやっと書くことができました。お正月休みでは終わらず成人式を含んだ3連休も最終日まで掛かってしまいました。?

はじめに

DI(Dependency Injection)はよく使われる手法ですが、テストの文脈で語られることが多いようです。
そしてテストの実現容易性のためにDI(Dependency Injection)を用いることの有用性は疑う余地がないでしょう。

しかし、実際にDIを用いる場合、どのような視点のDependencyであって、それがどのような理由でInjectionされるとのかという点でいくつかのバリエーションがあり、それについて考察することは有意義ではないでしょうか。

この記事では、特にテストを行うためのDIと言う文脈で、日頃考えていることを書き連ねていきます。少しでもお役に立てれば幸いです。

記事内の使用言語

使用する言語は、

  • UML
  • Swift

です。
最後にPlantUMLを一部のせました。

依存が密結合なDI

まず密結合、疎結合な依存1、というところから始めたいと思います。

DIとは依存(Dependency)をhas-aの関係で対象クラスに持たせる(注入する:Injection)ことです。

ここで言う依存とは注入可能な依存であって、すなわち下で述べる<<interface>>Protocolの形式を持つものです。

密結合な依存とはなんなのか。
まずシンプルな例をUML:クラス図を用いて説明します。

UML:クラス図

下の例では、Speakableという抽象にPersonが依存しています。
Speakableの<<interface>>とは抽象のことで、Swiftの場合はProtocolと考えて差し支えないでしょう。2
SpeakPolitely、SpeakRudelyはSpeakableを実装したオブジェクトで、SpeakableとしてPersonクラスに注入されます。
speak()は、Personクラスのnegotiate()から呼び出されて利用されますので、negotiate()を実行した時の挙動をSpeakPolitely、SpeakRudelyの注入によって選択できる(機能を差し替えられる)ことになります。

スクリーンショット 2020-01-11 14.58.56.png

PersonクラスとSpeakableの間は、ひし形の塗りつぶしであった記号と線で結ばれています。
これはUMLにおいてcompositionと呼ばれるものです。

compositionは、SpeakableはPersonの一部であり、さらにライフサイクルも同一である
ということを意味しています。3
そして、この関係が密結合しているDependencyだと考えています。

realizationとは抽象の実現を意味しています。ようするにProtocolを実装したクラスを作ったということですね。このケースでは、Speakableという抽象をSpeakPolitely、SpeakRudelyクラスとして実現していることになります。

次に実際にSwiftコードに落とし込んだものを見てみます。

Swiftコード

自分はDIを行う時のDependency、すなわちProtocolの記述をどこで行うのか、が大変重要だと思っています。
この場所によって密結合なのか疎結合なのか、設計意図を推し量ることができると思っているからです。
密結合の場合、同一のファイル内か、ファイルを分けたとしても非常に近い位置に配置することが一般的だと思います。
下記の例では、クラス図に書いたようにPersonクラスとSpeakableが密結合のため、同一のファイル内の配置しています。

import Foundation

protocol Speakable {
    func speak()
}

class Person {

    private var dependency: Speakable

    init(_ dependency: Speakable) {
        self.dependency = dependency
    }

    func negotiate() {
        dependency.speak()
    }
}

抽象の実現として、Speakableを継承したクラスを定義します。

class SpeakPolitely: Speakable {
    func speak() {
        print("こちらを減らせば、お安くなりませんか")
    }
}

class SpeakRudely: Speakable {
    func speak() {
        print("高いな〜")
    }
}

以下で依存を注入していきます。

let politePerson = Person(SpeakPolitely())
politePerson.negotiate()  // こちらを減らせば、お安くなりませんか

let rudePerson = Person(SpeakRudely())
rudePerson.negotiate()    // 高いな〜

ここで登場したクラスやプロトコルやプロトコルを実現したクラスは、基本的にPersonクラスに(たとえDIを用いられていても)密結合しています。(他のクラスにとって意味がない、と言い換えることもできるかも知れません)

依存が疎結合なDI

疎結合な依存とはどんなものでしょうか。
疎結合なクラス群というのは想像しやすいと思いますが、疎結合であると言うことはそもそもクラス間で関連が存在していると言うことです。
ですので、それこそが依存が疎結合な状態と考えて良いと思います。
しかしながらDIであると言うことは、単純に関連があるだけではありません。関連の先は、クラスではなく抽象である必要があります。4

下に示すクラス図では、在庫一覧クラスが、商品情報リストを得るために、対象となるユーザの会員番号、店舗の情報をそれぞれ異なる抽象(product, user, shop)から取得しています。

UML:クラス図

スクリーンショット 2020-01-13 14.37.55.png

在庫一覧クラスと各管理クラス抽象の間に引かれた線は、PersonクラスとSpeakableの間では、ひし形の塗りつぶしであった記号と線(composition)でしたが、こちらではただの実践のみです。
この線のことをUMLではassociationと呼び、各クラスとは密結合せず5、単純なメソッド呼び出しなどで関連していることを意味しています。当然ライフサイクルも異なります。

さらに各管理クラスはここには記述がない他のクラスのために情報の追加や、削除、更新機能を有しています。

DIの目的の違い

上記の2つの例は、密結合、疎結合という違いがありましたが、実は目的も異なっています。
それは依存を注入される側の機能を変更したいかどうかということです。依存が密結合なDIの例では、もともとの目的が機能の切り替えのためのDIを採用したということになりますので必須です。切り替えたくないのなら、そもそも分離することは無意味です。

依存が疎結合なDIの例では、実は機能を切り替えることはありません。もちろんそのような要件があってDIを用いることが必要な場合もあるでしょう。しかし、今回提示した例では現在のところDIが必要なようには見えません。

実は別に目的があるといことです。下のクラス図を見てください。

スクリーンショット 2020-01-13 14.39.08.png

この新しいクラス図では、各管理クラスの代わりにテスト用スタブに切り替えることができるようになっています。つまりDIの目的はテストであるということです。

何が問題なのか

みなさんは、機能要件を実現するための設計と、テストを目的にした設計を区別せずに、たまたま同じ設計でできるからと言う理由で、実施しまうのに違和感がありませんか?
私には後者の例においてはいくつかの問題点があると考えています。

不要な機能の注入

まず問題点としてあげられるのは、不要な機能が注入されているということです。
在庫一覧クラスは、注入されたクラスの一部の機能しか必要としていません。
在庫一覧で必要なのは各管理クラスから情報が取得できれば良いのであって、追加、更新、削除のメソッドは必要ありません。

もしテストしないのならばこれらのメソッドは呼び出せるけど使わないだけで終わってしまいますが、テストを考えるとそうはいきません。テストのために不必要なメソッドまで(たとえ中身が空であっても)実装する必要があります。これはクラスの規模が大きくなってくると無視できないほどの負荷になるでしょう。

目的と注入されたオブジェクトのメソッド名が一致しない(ことがある)

またテスト以外でも問題はあります。お互いに独立して外部にメソッドを公開しているために、呼び出す側の目的と、実際に呼びださなければならないメソッド名称の乖離が発生します。このことは、ソースコードを読解する上ではすくなからぬ障害になると考えます。

  • 呼び出す側の目的: 会員番号を利用したい
  • 呼び出すメソッド名: fetchData()

例があまり良くないですが、言わんとすることは分かっていただけるのではないかと思います。
自分自身のメソッドであれば、適切な名前を付けることも可能ですが、独立した外部のクラスのメソッド名はまったく別の理由で名付けられていることもあります。特に設計者・実装者が他人の場合は(ましてやスキルレベルや開発会社が異なっていたりすると)頻繁に遭遇するかもしれません。

依存が散逸する

依存した外部のプロトコルによる実装を直接呼び出すことで、依存が散逸します。
在庫一覧クラスの例で言えば、依存は変数product, user, shopに格納され在庫一覧クラスの各所で呼びだされたりします。
依存がproduct1つだけであればproductを検索すれば良いだけですが、依存が複数になりさらに上記のメソッド名が一致しない問題と相まってソースコードの読解はより難しくなっていくでしょう。

改善案

UML:クラス図

依存が密結合なDIと同様に在庫一覧はcompositionの関係にある在庫一覧DependencyProtocolを持ちます。この在庫一覧DependencyProtocolには、在庫一覧が依存するメソッドしか記述されていません。
このようにすることで、不要な機能の注入を避けることができます。

また目的と注入されたオブジェクトのメソッド名が一致しない問題も回避することができます。在庫一覧DependencyProtocolのメソッド名は在庫一覧クラスが自由に名付けられるからです。例えばこの例では、商品一覧取得であったものを在庫一覧取得などのように付け替えています。

依存が散逸する問題も在庫一覧DependencyProtocolに依存が集められたことで、在庫一覧が外部に依存するすべての項目が一覧できるようになります。

スクリーンショット 2020-01-13 14.40.11.png

Swiftコード

Swiftでは下記のように書けると思います。

protocol StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product]
    func getMember() -> Member
    func getShopInfo() -> Shop
}

class StockItems {
    private var dependency: StockItemsDependencyProtocol = StockItemsDependency()

    init(_ dependency: StockItemsDependencyProtocol) {
        self.dependency = dependency
    }
    func showList() {
        let list = dependency.getStockItems(member: dependency.getMember(), shop: dependency.getShopInfo())
        // 在庫一覧描画
    }
}

class StockItemsDependency: StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product] {
        return ProductManager().getProducts(member: member, shop: shop)
    }
    func getMember() -> Member {
        return MemberManager().getMember()
    }
    func getShopInfo() -> Shop {
        return ShopManager().getShop()
    }
}

疎結合の外部クラスの定義です。

class ProductManager {
    func addProduct(product: String) { }
    func getProducts(member: Member, shop: Shop) -> [Product] {
        return [Product]()
    }
    func updateProduct() { }
    func deleteProduct() { }
}

class MemberManager {
    func addMember(member: Member) { }
    func getMember() -> Member {
        return Member()
    }
    func updateMember() { }
    func deleteMember() { }
}

class ShopManager {
    func addShop(shop: Shop) { }
    func getShop() -> Shop {
        return Shop()
    }
    func updateShop() { }
    func deleteShop() { }
}
}

テストのためのDI

テストでは、下のクラス図のように「在庫一覧DependencyProtocol」の実現としてテストスタブを追加します。

UML:クラス図

スクリーンショット 2020-01-13 14.40.53.png

この手法にも何点かの問題があります。
その一つが、不要な機能の注入です。あれっと思われると思いますが、この場合の不要なはテストにとってと言うことです。
今回の例はシンプルすぎて発生しませんが、例えば在庫一覧クラスの特定のメソッドをテストするときに、在庫一覧DependencyProtocolの特定のメソッドしか必要がない場合、他の全てのメソッドを実装することになると面倒です。このような問題を回避するための手法として下記のような書き方を考えました。

Swiftコード

StockItemsDependencyProtocolのprotocol extensionを作成しますが、基本的にメソッドを(プロパティがある場合はプロパティも)実装しないで使用するとassertします。ただし、呼びださなければ当然assertしません。

extension StockItemsDependencyProtocol {
    func getStockItems(member: Member, shop: Shop) -> [Product] {
        assert(false)
        return  [Product]()
    }
    func getMember() -> Member {
        assert(false)
        return Member()
    }
    func getShopInfo() -> Shop {
        assert(false)
        return Shop()
    }
}

このようなprotocol extensionを持った状態で、StockItemsのgetMember()に依存する機能だけをテストしようとすると下記のように書くことができます。

class StockItemsTests: XCTestCase {
    // ここでDependencyを実装し、
    func testGetMemberNumber() {
         class StockItemsDependencyTestStub: StockItemsDependencyProtocol {
             func getMember() -> Member {
                 return Member(number: 27809, type: "paied")
             }
         }

        let stockItems = StockItems()
        stockItems.dependency = StockItemsDependencyTestStub() // ここで注入し、
        let menberNumber = stockItems.getMemberNumber() // テストする
        XCTAssertEqual(menberNumber, "27809")
    }
}

テストに必要なgetMember()だけを実装して、StockItemsクラスのgetMemberNumber()をテストします。
不要なメソッド等の実装が必要なく、テストに必要なすべてが集約されて記述されているので、実装者以外のエンジニアが見ても分かりやすくできていると思います。

またもし内部の実装が変更などされ、StockItemsクラスがgetMemberNumber()内でgetMember()以外を使うようになった場合、protocol extensionの記述にあるようにassertするのでテストが不完全であることがすぐに分かります。

プロダクションコードを汚していないのも、プロダクションコードに必要なコードがやはりStockItemsクラスに集約して記述できることもとても大きなポイントだと思います。

このようにプロダクションコードと、テストコードを分離し、記述するコードを極力減らすことでテストを書く負担が少しでも減少すればと考えています。

このさき

ここまで書きたいことの90%は終わっています。
しかし、この先判断がつきにくい問題があるので書きておきます。
まずはSwiftコードをご覧ください。

protocol StockItemsDependencyProtocol {
    func getStockItems(memberNumber: Int, shop: Shop) -> [Product]
    func getMemberNumber() -> Int
    func getShopInfo() -> Shop
}

class StockItems {
    private var dependency: StockItemsDependencyProtocol = StockItemsDependency()

    init(_ dependency: StockItemsDependencyProtocol) {
        self.dependency = dependency
    }
    func showList() {
        let list = dependency.getStockItems(memberNumber: dependency.getMemberNumber(), shop: dependency.getShopInfo())
        // 在庫一覧描画
    }
}

class StockItemsDependency: StockItemsDependencyProtocol {
    func getStockItems(memberNumber: Int, shop: Shop) -> [Product] {
        return ProductManager().getProducts(memberNumber: memberNumber, shop: shop)
    }
    func getMemberNumber() -> Int {
        // グルーロジック
        return MemberManager().getMember().number
    }
    func getShopInfo() -> Shop {
        return ShopManager().getShop()
    }
}

StockItemsDependencyProtocolが期待する依存を、外部のクラスが直接提供できない場合があります。
できないと言うより、StockItemsDependencyにロジックを置くことで更にプロダクションコードが明瞭になることがあります。
上記の例では、StockItemsは、メンバー番号を期待しており、MemberManagerは、Memberクラスを返すことしかしません。このような場合、StockItemsDependencyには、グルーロジック、つまり糊付けロジックが存在することになります。

何が問題かというと、テストにおいて注入されるのはStockItemsDependencyProtocolであるために、テスト時にはグルーロジックごと抹消されてこの部分がテストされない、ということです。カバレッジを100%にしたい場合は採用できませんが、効率を優先するならばMemberクラスを生成するより、Int値を返すスタブを作成する方が遥かに楽です。
ここのカバレッジか効率化はプロジェウトの性格にもよると思いますが、非常に短期間の開発を要求される場合は、一考に値すると思います。

おまけ

PlantUML

最後に、最後のクラス図だけ載せてきます。
iOSの開発ではUMLを持ち出すことがあまりないのですが、本記事のようなものを提示する場合は便利かも知れません。
気になる方はこちらもどうぞ
設計ドキュメント(UML)をPlantUMLで書いてみる

@startuml

class 在庫一覧
在庫一覧 : - dependency: 在庫一覧DependencyProtocol
在庫一覧 : + 在庫一覧描画()

interface 在庫一覧DependencyProtocol << interface >>
在庫一覧DependencyProtocol : + 在庫一覧取得(会員,店舗)
在庫一覧DependencyProtocol : + 会員取得()
在庫一覧DependencyProtocol : + 店舗取得()

在庫一覧 "1" *-- "1" 在庫一覧DependencyProtocol : composition
在庫一覧DependencyProtocol -[hidden]ri- 在庫一覧

class 在庫一覧Dependency
在庫一覧Dependency :  + 在庫一覧取得(会員,店舗)
在庫一覧Dependency :  + 会員取得()
在庫一覧Dependency :  + 店舗取得()

在庫一覧DependencyProtocol <|.. 在庫一覧Dependency : realization

class 在庫一覧Dependencyテストスタブ
在庫一覧Dependencyテストスタブ :  + 在庫一覧取得(会員,店舗)
在庫一覧Dependencyテストスタブ :  + 会員取得()
在庫一覧Dependencyテストスタブ :  + 店舗取得()

在庫一覧DependencyProtocol <|.. 在庫一覧Dependencyテストスタブ : realization

在庫一覧Dependency  "1" -- "1" 商品管理DB : association
在庫一覧Dependency  "1" -- "1" 会員管理DB : association
在庫一覧Dependency  "1" -- "1" 店舗管理 : association

note bottom of 在庫一覧Dependencyテストスタブ : "追加"


class 商品管理DB
商品管理DB : + 商品追加()
商品管理DB : + 商品一覧取得(会員,店舗)
商品管理DB : + 商品取得()
商品管理DB : + 商品更新()
商品管理DB : + 商品削除()


class 会員管理DB
会員管理DB : + 会員追加()
会員管理DB : + 会員取得()
会員管理DB : + 会員更新()
会員管理DB : + 会員削除()


class 店舗管理
店舗管理 : + 店舗追加()
店舗管理 : + 店舗取得()
店舗管理 : + 店舗更新()
店舗管理 : + 店舗削除()

@enduml

  1. ちなみにこの言い方はオリジナルなものなので、もし一般的な概念として呼称があるようならば教えていただけると嬉しいです。 

  2. Swiftには抽象クラス(abstract)という概念がないと思いますが、Javaでは抽象クラスの注入もありえます。 

  3. 【コードで分かるUMLシリーズ】クラス図の書きかた(集約とコンポジションの意味の違い) 

  4. 厳密には抽象である必要はないかも知れませんが、その場合はDIの意味がなさそうです。 

  5. あいまいな表現になっていますが、密結合とcompositionは別の概念です 

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

[iOS]what3words API を触ってみた

what3words とは

what3words とは住所などで表現される位置情報を、3つの単語で表現できるサービスです。
地球上を 3m × 3m のマスに区切り、その1マスを3つの単語で指定することができます。

「住所と違って何がいいの?」と思うかもしれませんが、
お花見の場所取りや、大型ショッピングセンターの特定の入り口など、
住所だけでは説明できない位置情報を短い3つの単語だけで指定することができます。

「それなら緯度経度の座標でいいやん」って思うかもしれませんが、
誰かに共有する際、緯度経度は不規則な数字であるため
コピー&ペースト以外の方法では共有が難しいというデメリットがあります。

what3word を使用すると、3つの単語を共有するだけで位置情報を共有できるので、
座標よりサクッと共有できるといったメリットがあります。

「住所とか緯度経度とか長いよね、カーナビの行き先指定とかめんどくさいし。
what3words を使うと3つの単語でサクッと位置情報を指定、共有できちゃうよ」
って感じです。

開発環境

  • Xcode:11.1(11A1027)
  • Swift 5
  • iOS:13.1

下準備

こちらの手順に沿って進めていきます

API キーの取得

  1. こちらから利用登録をする
  2. ログイン後、Developer API Keys を選択 スクリーンショット 2020-01-03 17.05.02.png
  3. Create API Key を選択 スクリーンショット 2020-01-03 17.05.12.png
  4. Name、Description、Country を入力し、API Key を作成 スクリーンショット 2020-01-03 17.05.25.png
  5. 以下の画像の矢印で示した所に表示されているものが API Key になります スクリーンショット 2020-01-03 17.05.34.png

what3words のインストール

  • CocoaPods
platform :ios, '9.0'
use_frameworks!

target 'MyApp' do
    pod 'what3words', :git => 'https://github.com/what3words/w3w-swift-wrapper.git'
end      
  • Charthage
github "what3words/w3w-swift-wrapper"

セットアップ

  • API Key のセット

AppDelegate.swift で API Key の登録を行います

AppDelegate.swift
import what3words

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    W3wGeocoder.setup(with: "input your API key")
    return true
}
  • what3words の API を使用したいところに以下を記述します
import what3words
import CoreLocation

以上で what3words を使用するための準備は完了です

what3words を使ってみた

今回は、

  1. 現在地の 3words を確認
  2. 検索欄から 3words を検索して位置を確認

の2つの機能を試してみようと思います

1. 現在地の 3words を確認

現在地の 3words を確認するために、まずは Google Maps SDK for iOS を使用し、
現在地の取得、表示を行います

Google Maps SDK for iOS を使用して現在地を地図上に表示にするまでの実装は別記事でまとめようと思います

ViewController.swift
import UIKit
import what3words
import GoogleMaps

class ViewController: UIViewController {

    /// Google Map
    let mapView: GMSMapView = {
        let camera = GMSCameraPosition.camera(withLatitude: 35.6812226, longitude: 139.7670594, zoom: 12.0)
        let view = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
        view.isMyLocationEnabled = true
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    /// 現在地の 3words を表示するラベル
    let what3wordsLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 1
        label.textAlignment = .left
        label.textColor = .black
        label.font = UIFont.boldSystemFont(ofSize: 17.0)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(mapView)
        view.addSubview(what3wordsLabel)

        searchBar.delegate = self
        requestLoacion()
    }

    override func viewWillLayoutSubviews() {
        setupSubViews()
    }

    private func setupSubViews() {
        NSLayoutConstraint.activate([
            what3wordsLabel.topAnchor.constraint(equalTo: view.top, constant: view.safeAreaInsets.top + 16.0),
            what3wordsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16.0),

            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        searchBar.delegate = self
    }

    private func requestLoacion() {
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
    }
}

extension ViewController: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            // 現在の位置情報を取得
            locationManager.startUpdatingLocation()
        case .denied, .notDetermined, .restricted:
            print("許可されていません")
        @unknown default:
            fatalError()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let currentLocation = locations.last?.coordinate else { return }

        let camera = GMSCameraPosition(latitude: currentLocation.latitude,
                                       longitude: currentLocation.longitude,
                                       zoom: 17.0)
        mapView.animate(to: camera)
        W3wGeocoder.shared.convertTo3wa(coordinates: currentLocation, language: "ja", completion: { [weak self] (place, error) in
            DispatchQueue.main.async {
                // place.words に 位置情報から変換した 3words を取得できる
                self?.what3wordsLabel.text = place?.words
            }
        })
        locationManager.stopUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(error.localizedDescription)
    }
}

2. 検索欄から 3words を検索して位置を確認

次に画面上に検索欄を表示させ、そこに 3words を入力し、Map 上にピンを立てるようなことをしたいと思います

what3words の検索 API の使用上、3つの単語は . で区切る必要があるようなので、
検索バーでは入力しやすいよう、スペース区切りで入力してもらって、それを . 区切りに変換するような処理を行なっています

ViewController
class ViewController: UIViewController {
    // 追記
    /// what3words を入力する検索バー
    let searchBar: UISearchBar = {
        let view = UISearchBar()
        view.placeholder = "スペース区切りで3つの単語を入れてください"
        view.backgroundColor = .white
        view.barTintColor = .black
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(mapView)
        view.addSubview(what3wordsLabel)
        view.addSubview(searchBar) // 追記

        searchBar.delegate = self // 追記
        requestLoacion()
    }

    override func viewWillLayoutSubviews() {
        setupSubViews()
    }

    /// view の layout を指定
    private func setupSubViews() {
        // レイアウトを修正しています
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.topAnchor, constant: view.safeAreaInsets.top + 16.0),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            what3wordsLabel.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 16.0),
            what3wordsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16.0),

            mapView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

// ~~~~~~~~~省略~~~~~~~~~~~~

extension ViewController: UISearchBarDelegate {

    /// 検索ボタンをタップした際に呼び出されるメソッド
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        // スペース区切りで入力したテキストを . 区切りに変換
        let what3words = text.replacingOccurrences(of: " ", with: ".")

        // ex) "ふつか そうさ ぜんぜん" で皇居へ移動します
        W3wGeocoder.shared.convertToCoordinates(words: what3words, completion: { [weak self] (place, error) in
            guard let location = place?.coordinates else { return }
            let camera = GMSCameraPosition(latitude: location.latitude, longitude: location.longitude, zoom: 17.0)
            let marker = GMSMarker(position: location)
            marker.title = what3words
            DispatchQueue.main.async {
                marker.map = self?.mapView
                self?.mapView.animate(to: camera)
            }
        })
    }
}

コードの全容

ViewController.swift
import UIKit
import what3words
import GoogleMaps

class ViewController: UIViewController {

    /// Google Map
    let mapView: GMSMapView = {
        let camera = GMSCameraPosition.camera(withLatitude: 35.6812226, longitude: 139.7670594, zoom: 12.0)
        let view = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
        view.isMyLocationEnabled = true
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    /// 現在地の 3words を表示するラベル
    let what3wordsLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 1
        label.textAlignment = .left
        label.textColor = .black
        label.font = UIFont.boldSystemFont(ofSize: 17.0)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    /// what3words を入力する検索バー
    let searchBar: UISearchBar = {
        let view = UISearchBar()
        view.placeholder = "スペース区切りで3つの単語を入れてください"
        view.backgroundColor = .white
        view.barTintColor = .black
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    let locationManager = CLLocationManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(mapView)
        view.addSubview(what3wordsLabel)
        view.addSubview(searchBar)

        searchBar.delegate = self
        requestLoacion()
    }

    override func viewWillLayoutSubviews() {
        setupSubViews()
    }

    /// view の layout を指定
    private func setupSubViews() {
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.topAnchor, constant: view.safeAreaInsets.top + 16.0),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            what3wordsLabel.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 16.0),
            what3wordsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16.0),

            mapView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

    private func requestLoacion() {
        locationManager.delegate = self
        // 位置情報を取得
        locationManager.requestWhenInUseAuthorization()
    }
}

extension ViewController: CLLocationManagerDelegate {

    /// 位置情報の取得の認可状態が更新された際に呼び出されるメソッド
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            locationManager.startUpdatingLocation()
        case .denied, .notDetermined, .restricted:
            print("許可されていません")
        @unknown default:
            fatalError()
        }
    }

    /// 位置情報が更新された際に呼び出されるメソッド
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let currentLocation = locations.last?.coordinate else { return }

        let camera = GMSCameraPosition(latitude: currentLocation.latitude,
                                       longitude: currentLocation.longitude,
                                       zoom: 17.0)
        mapView.animate(to: camera)
        W3wGeocoder.shared.convertTo3wa(coordinates: currentLocation, language: "ja", completion: { [weak self] (place, error) in
            DispatchQueue.main.async {
                // 現在の座標から変換した 3words をラベルに表示する
                self?.what3wordsLabel.text = place?.words
            }
        })
        locationManager.stopUpdatingLocation()
    }

    /// 位置情報の取得に失敗した際に呼び出されるメソッド
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print(error.localizedDescription)
    }
}

extension ViewController: UISearchBarDelegate {

    /// 検索ボタンをタップした際に呼び出されるメソッド
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        // スペース区切りで入力したテキストを . 区切りに変換
        let what3words = text.replacingOccurrences(of: " ", with: ".")

        // ex) "ふつか そうさ ぜんぜん" で皇居へ移動します
        W3wGeocoder.shared.convertToCoordinates(words: what3words, completion: { [weak self] (place, error) in
            guard let location = place?.coordinates else { return }
            let camera = GMSCameraPosition(latitude: location.latitude, longitude: location.longitude, zoom: 17.0)
            let marker = GMSMarker(position: location)
            marker.title = what3words
            DispatchQueue.main.async {
                marker.map = self?.mapView
                self?.mapView.animate(to: camera)
            }
        })
    }
}

おわりに

ここに記載したコードは github にも上げているので、よかったらご参考ください
https://github.com/nwatabou/TestWhat3Words

※注意事項

Google Maps SDK の key を github に上げないようにしているので、clone しただけではビルドが通らないと思います
APIKeysConstants.swift というファイルを新規で追加していただいて、そこに以下のようにお使いの key を記述していただくとビルドが通るようになるかと思います

APIKeysConstants.swift
let GOOGLE_MAPS_API_KEY = "{Your API key}"

参考

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

【swift】FSCalendarのテーマカラーをコード上から変更したい!

やりたいこと

タイトルの通り。
FSCalendarのカラーをコードで変更したい。

環境

xcode 11.3
swift 5.1.3

実装方法

// storyboardから繋いであるFSCalendar
@IBOutlet weak var calendar: FSCalendar!

// calendarの色の設定
calendar.appearance.todayColor = UIColor.red
calendar.appearance.headerTitleColor = UIColor.red
calendar.appearance.weekdayTextColor = UIColor.red

calendar.appearanceの後に続けて、以下の写真のようにstoryboard上で表示されているプロパティ名をかけばいい。
スクリーンショット 2020-01-13 18.26.38.png

テーマカラーとして設定するには、todayColorとtitleColorとweekdayTextColorを変えればいいかな。
その三つをUIColor.systemPurpleに設定したのが、以下

スクリーンショット 2020-01-13 18.26.56.png

まとめ

記事を色々探すより、ソースコードをきちんと読むの、だいじ。

参考文献

github : WenchaoD/FSCalendar

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

【swift】FSCalendarの諸々の色をコード上から変更したい!

やりたいこと

タイトルの通り。
FSCalendarのカラーをコードで変更したい。

環境

xcode 11.3
swift 5.1.3

実装方法

// storyboardから繋いであるFSCalendar
@IBOutlet weak var calendar: FSCalendar!

// calendarの色の設定
calendar.appearance.todayColor = UIColor.red
calendar.appearance.headerTitleColor = UIColor.red
calendar.appearance.weekdayTextColor = UIColor.red

calendar.appearanceの後に続けて、以下の写真のようにstoryboard上で表示されているプロパティ名をかけばいい。
スクリーンショット 2020-01-13 18.26.38.png

テーマカラーとして設定するには、todayColorとtitleColorとweekdayTextColorを変えればいいかな。
その三つをUIColor.systemPurpleに設定したのが、以下

スクリーンショット 2020-01-13 18.26.56.png

まとめ

記事を色々探すより、ソースコードをきちんと読むの、だいじ。

参考文献

github : WenchaoD/FSCalendar

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

レスポンスのJSONが異なるAPIにオプショナルを使わず対応する方法(Swift)

※最後に記載していますが、この記事は未完成です。もう少しお待ちください

はじめに

成功時と失敗時で、レスポンスのJSONの構造が異なるAPIはよくあると思います。

ここでは Currencylayer のAPIを例に考えてみます。

http://apilayer.net/api/live?access_key={アクセスキー}&source={通貨}&format=1
成功時
{
  "success":true,
  "terms":"https:\/\/currencylayer.com\/terms",
  "privacy":"https:\/\/currencylayer.com\/privacy",
  "timestamp":1578845345,
  "source":"USD",
  "quotes":{
    "USDAED":3.673204,
    "USDZWL":322.000001
  }
}
失敗時
{
  "success":false,
  "error":{
    "code":105,
    "info":"Access Restricted - Your current Subscription Plan does not support Source Currency Switching."
  }
}

対応方法はいくつかあると思いますが、私はどちらか片方にのみ存在するキーをオプショナル型にする方法を考えました。
(使わないキーは省略しています)

ExchangeRatesDTO.swift
struct ExchangeRatesDTO: Decodable {
    // 共通
    let success: Bool // オプショナル型にしなくていい

    // 成功時のみ
    let source: String?
    let quotes: [String: Double]?

    // 失敗時のみ
    let error: CurrencylayerError?

    struct CurrencylayerError: Decodable {
        let code: Int
        let info: String
    }
}

デコード時は success の値で分岐させます。

CurrenylayerProvider.swift
let data = #"""
{
  "success":true,
  "terms":"https:\/\/currencylayer.com\/terms",
  "privacy":"https:\/\/currencylayer.com\/privacy",
  "timestamp":1578845345,
  "source":"USD",
  "quotes":{
    "USDAED":3.673204,
    "USDZWL":322.000001
  }
}
"""#.data(using: .utf8)!

do {
    let dto = try JSONDecoder().decode(ExchangeRatesDTO.self, from: data)
    if !dto.success {
        fatalError()
    }
    // 成功時の処理をここに記述する
    } catch {
        fatalError("error: \(error)")
    }
}

しかし、これだと以下のデメリットがあります。

  • オプショナル型なので呼び出すたびにアンラップが必要になる
  • 成功時と失敗時でJSONの構造がコードから読み取れない
    • コメントでわかりやすくすることはできる

列挙型を使ってデメリットを解決する方法を @takasek さんから伺ったので、備忘録として残します。

列挙型を使ってオプショナルを消す

大変ありがたいことに、Twitterで教えていただきました。

こちらを元に、上記のコードを改善します。

…と思ったのですが、 自分の実力ではすぐにはできませんでした
途中まで記述したコードを載せますが、見当違いのことをしているかもしれません。

ExchangeRatesDTO.swift
struct ExchangeRatesSuccessDTO: Decodable {
    let success: Bool
    let source: String
    let quotes: [String: Double]

    enum CodingKeys: String, CodingKey {
        case success
        case source
        case quotes
    }
}

struct ExchangeRatesFailureDTO: Decodable {
    let success: Bool
    let error: CurrencylayerError

    enum CodingKeys: String, CodingKey {
        case success
        case error
    }
}

enum ExchangeRatesDTO: Decodable {
    case success(ExchangeRatesSuccessDTO)
    case failure(ExchangeRatesFailureDTO)

    private enum CodingKeys: String, CodingKey {
        case success
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let success = try container.decode(Bool.self, forKey: .success)
        switch success {
        case true:
            let successContainer = try decoder.container(keyedBy: ExchangeRatesSuccessDTO.CodingKeys.self)
            let source = try successContainer.decode(String.self, forKey: .source)
            let quotes = try successContainer.decode([String: Double].self, forKey: .quotes)
            self = .success(ExchangeRatesSuccessDTO(success: success, source: source, quotes: quotes))
        case false:
            let failureContainer = try decoder.container(keyedBy: ExchangeRatesFailureDTO.CodingKeys.self)
            let error = try failureContainer.decode(CurrencylayerError.self, forKey: .error)
            self = .failure(ExchangeRatesFailureDTO(success: success, error: error))
        }
    }
}
CurrenylayerProvider.swift
// TBD

おわりに

行き詰まったので一旦ここまでにします。
わかる人がいたら教えていただけると嬉しいです?

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

@_functionBuilderを理解してSwiftUIイリュージョンのタネをあかす

はじめに

最近ようやく少しずつSwiftUIに触れてみようかなと思い始めました。

SwiftUIとは、宣言的なUI実装を実現した、
近い将来iOSのUI描画のメインストリームにもなるであろうフレームワークです。

まずはこれを見てください。

いい感じ.png

画像のソース
import SwiftUI

struct SwiftUIView: View {
    var body: some View {
        VStack {
            Text("SwiftUIイリュージョン")
            Text("このコードだけでいい感じに表示されるのはなぜ?")
        }
    }
}

シンプルかつ直感的に理解しやすいコードで画像のUIを描画できてしまっています。
その様子はさながらイリュージョンです。

しかしながら、近い将来このイリュージョンAPIを使いこなす必要が出てくる可能性は高く、
イリュージョンを放ってはおけないと思い、この記事を書くに至りました。

また、イリュージョンのタネが含まれる箇所は複数ありますが、
この記事では個人的に最もイリュージョンを感じた以下の部分のタネをあかします。

BESTイリュージョニスト
VStack {
    Text("SwiftUIイリュージョン")
    Text("このコードだけでいい感じに表示されるのはなぜ?")
}

このコードは一体なんなのでしょうか。
UIKit時代を生きてきた筆者には当初受け入れがたいコードでしたが、
このタネはどうやら@_functionBuilderにあるらしいのです。

@_functionBuilderの解説

VStackの定義を確認してみましょう。
子Viewを縦に並べてくれるViewであることがわかります。
UIKitでいうNSLayoutConstraint.Axis = .vertical設定のUIStackviewです。

/// A view that arranges its children in a vertical line.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct VStack<Content> : View where Content : View {

    /// Creates an instance with the given `spacing` and Y axis `alignment`.
    ///
    /// - Parameters:
    ///     - alignment: the guide that will have the same horizontal screen
    ///       coordinate for all children.
    ///     - spacing: the distance between adjacent children, or nil if the
    ///       stack should choose a default distance for each pair of children.
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = Never
}

@ViewBuilder...?
圧倒的にイリュージョンに関わっていそうな何かがいることがわかります。
教科書的にはこんな説明でした。

概要
A custom parameter attribute that constructs views from closures.

ユースケース
You typically use ViewBuilder as a parameter attribute for child view-producing closure parameters, allowing those closures to provide multiple child views.

クロージャーで子Viewを生成できるパラメータ属性だと言っているようです。

今度はこの@ViewBuilderの定義を見てみましょう。

@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
    /// unmodified.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

こちらにもあきらかに怪しいパラメータ属性がいますね。

そうなんです。
この@_functionBuilderこそがイリュージョンの生みの親のようで、
Swift5.1で導入されました。

@_functionBuilder属性のViewBuilderbuildBlockメソッドの引数に渡すことで、
VStackが子ViewのText("hoge")を生成した
、というのがイリュージョンのタネです。

もう少し踏み込んでみましょう。
今回の例に沿えば、@_functionBuilderの仕組みは以下のように説明できます。

1.ViewBuilder@_functionBuilder属性を付ける(→@ViewBuilder属性が使えるようになる)
2.VStackinitのクロージャー引数content@ViewBuilder属性を付ける
3.そのクロージャーに渡すText("hoge")ViewBuilderbuildBlockメソッドに渡されるようになる

ちなみに、複数の子Viewを生成するケースはViewBuilderextensionに定義されています。
子Viewは最大で10個生成することができるようです。

2個
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View



10個
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View

イリュージョンはイリュージョンではなかった

今回取り上げたイリュージョンはつまるところ、
@_functionBuilder属性のViewBuilderbuildBlockメソッド
によって成り立っていました。

これに加えてクロージャーのreturnも省略せずに書き直すと、最初の画像のソースは、
UIKit時代を生きた筆者にも理解しやすいものになりました。

画像のソース(NOイリュージョン)
import SwiftUI

struct SwiftUIView: View {
    var body: some View {
        return VStack {
            return ViewBuilder.buildBlock(
                Text("SwiftUIイリュージョン"),
                Text("このコードだけでいい感じに表示されるのはなぜ?")
            )
        }
    }
}
タネあかし前
VStack {
    Text("SwiftUIイリュージョン")
    Text("このコードだけでいい感じに表示されるのはなぜ?")
}
タネあかし後
return VStack {
    return ViewBuilder.buildBlock(
        Text("SwiftUIイリュージョン"),
        Text("このコードだけでいい感じに表示されるのはなぜ?")
    )
}

イリュージョンのように見えたSwiftUIのコードも、蓋を開けてみれば、
Appleが開発者のためにラップしたり省略を促したりしたAPIであるとわかりました。

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

テスト時のみEquatableプロトコルに準拠させる方法(Swift)

はじめに

「製品コードでは構造体を比較しないけど、テスト時は比較したい」ということはありませんか:interrobang:

Equatable プロトコルに準拠することで、構造体同士を == で比較できるようになります。

FooEntity.swift
struct FooEntity: Equatable {
    let name: String
    let age: Int
}
FooTests.swift
let fooEntity1 = FooEntity(name: "foo", age: 18)
let fooEntity2 = FooEntity(name: "foo", age: 24)

// `Equatable` に準拠しているため、ビルドが通る
XCTAssertEqual(fooEntity1, fooEntity2) // `age` が異なるのでテストは失敗する

このように製品コードで直接 Equatable プロトコルに準拠させる方法が考えられますが、「 テスト時のみ比較したい 」という意図が読み取れません。

Twitterでテスト時のみ Equatable プロトコルに準拠させる方法を教えていただいたので、備忘録として残します。

個人的な結論:テスト時のみEquatableプロトコルに準拠させる必要はない

本題に入る前にまず私の結論を言います。
本末転倒ですが、Twitterでいろいろ意見を頂いた結果、私は製品コードで Equatable プロトコルに準拠させればいいと結論付けました。
理由は以下の通りです。

  • Equatable プロトコルに準拠しても副作用がほとんどない
    • コンパイラが自動生成する == のコードによるオーバーヘッドくらい
  • テスト時のみ Equtable プロトコルに準拠させる方法はどれもデメリットがある
    • 詳しくは後述する
  • 「構造体が比較できる」ことを当たり前の言語仕様と考えれば、製品に不要なコードが含まれていることにはならない
    • StringやIntも Equatable プロトコルに準拠しており、比較しなくても普通に使っている

方法①:テスト用のマクロを作る

@_ha1f さんから教えていただきました。

テスト用のマクロを作り、テスト時のみエクステンションで準拠させます。

FooEntity.swift
struct FooEntity {
    let name: String
    let age: Int
}

#if TEST
extension FooEntity: Equatable {}
#endif

これは思いつかなかったので目から鱗でした。
「テスト時のみ比較する」という意図が明確にわかります。

ただし、「 マクロが増える 」というデメリットがあります。

方法②:テストターゲットで準拠させる

方法①のエクステンションをテストターゲット側に記述すれば、マクロが不要になります。

FooTests.swift
@testable import FooTarget

extension FooEntity: Equatable {
    public static func == (lhs: FooEntity, rhs: FooEntity) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

しかし、 == のコードを自分で実装し、 public にする必要があります。
(なぜその必要があるかの言語仕様までは調べていません)

これは「 構造体に新しくプロパティを追加したときに、修正が必要になる 」というデメリットがあります。
修正しなくても警告やエラーが発生しないため、忘れがちになります。

おわりに

方法①も②もデメリットがあることがわかりました。
私はできる限りテスト時のみ使うコードを製品に含めたくないので、デメリットがなければ採用していました。

Twitterで議論が広がると、自分だけでは考えつかなかったことを学べるので、とてもありがたいです。
他にご意見のある方がいらっしゃれば、遠慮なくコメントやTwitterでご連絡ください!

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

[はじめてのiOSアプリ]xcodeで地図アプリを作成(その9)

はじめに

iOSアプリを作ってみたいけど
何から始めて良いのかわからない

とりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思う

という記事の9回目です。

今回は、いままでやってきたことを git でソースコード管理し、GitHub にて公開までします。
とりあえず、今回のシリーズは、これで終了の予定。

懺悔(ざんげ)

  • 今回のシリーズは、アプリを「作ってみる」ことが主眼だったので git に関することは「あとまわし」だったけど、本当であれば「最初」にすることだと思います
  • そのようなこともあり、今回の記事は、あとからgitでソースコード管理し、GitHubと連携することになります
  • 一部の方には参考になると思いますが、多くの方には参考になりません

GitHubアカウントの作成

  • ブラウザで、ここ↓にアクセス
    • https://github.com/join
    • 手順などの詳細は、他の記事を参照してください
    • 簡単に作成できるはずです

ソースコード管理対象の確認

  • 【なぜ?】
    • 権利(著作権など)の侵害は、法律違反
      • 違反すると罰せられる
      • 他人のものを、自分のもののように公開するのは、格好が悪くない?
  • 手順

    • 「ターミナル」アプリを起動
      • Launchpadからでも、アプリメニューからでも、好きな方法で起動する
        launchpad_terminal.png
    • 「ターミナル」にて以下のように、プロジェクトフォルダ全体に対し検索
      • 【なぜ?】
        • cd で地図アプリ(MyGpsMap)のプロジェクトフォルダへ移動している
          • プロジェクトフォルダの位置に関しては、この記事を参照
        • grep コマンドは、テキストファイル内の文字列を検索するツール
        • -r オプションは、サブフォルダ全てを検索対象と指定している
        • -i オプションは、大文字小文字を区別せず検索することを指定している
        • 'copyright' は、検索文字列を指定している
        • 最後の * は、全てのファイルが対象であることを指定している
    console
    % cd $HOME/github/shinobee/MyGpsMap/
    % grep -r -i 'copyright' *           
    
    • 実行結果は、以下の通り
      • 他者(shinobee以外)が copyright を持っているファイルが見つからない
      • プロジェクトフォルダ全体を公開しても大丈夫
      • プロジェクトフォルダ全体を公開してくれた方が、使う(git cloneする)人もファイルの過不足が気にならないので「楽」だと思います
    console
    MyGpsMap/ViewController.swift://  Copyright © 2019 shinobee. All rights reserved.
    MyGpsMap/AppDelegate.swift://  Copyright © 2019 shinobee. All rights reserved.
    MyGpsMap/SceneDelegate.swift://  Copyright © 2019 shinobee. All rights reserved.
    Binary file MyGpsMap.xcodeproj/project.xcworkspace/xcuserdata/shinobee.xcuserdatad/UserInterfaceState.xcuserstate matches
    MyGpsMapTests/MyGpsMapTests.swift://  Copyright © 2019 shinobee. All rights reserved.
    MyGpsMapUITests/MyGpsMapUITests.swift://  Copyright © 2019 shinobee. All rights reserved.
    

GitHubリポジトリの作成

  1. GitHubにログイン

    • ブラウザで https://github.com/login を表示
    • 【なぜ?】
      • OSS(Open Source Software)のプロジェクトは、GitHubで公開で間違いないから
      • GitHubでのプロジェクト作成は、ブラウザを使うから
        • 自分は、他の手段を知らない
  2. GitHubにリポジトリを新規作成

    • 画面右上の「+」をクリックし、「New repository」を選択
    • 以下のようにレポジトリ情報を入力
      • [Repository name]は、MyGpsMap
      • [公開範囲]は、Public
        • このプロジェクトは「公開」する
        • 必要に応じて「非公開 Private」を選択しても可
      • [Initialize this repository with a README]は、チェックを入れないでOK
      • [.gitignore]は、変更しないでOK
      • [Add a license]は、変更しないでOK github-create-new-repository.png
    • [Create repository]ボタンをクリックして、しばらく待つ
      • 作成されたら、以下↓のように表示される github-created-new-repository.png

XcocdプロジェクトとGitHubリポジトリの連携

  1. GitHubアドレスを登録

    • 「ターミナル」にて以下のように、コマンドを実行
      • 【なぜ?】
        • GtHubでリポジトリを作成したあとに表示された「Quick setup」に書かれていたから
        • このコマンドを実行することで、GitHubのリポジトリを連携づけできる
        • 『プロジェクトに教えてあげる』と考えるほうが理解しやすいかも
    console
     % cd $HOME/github/shinobee/MyGpsMap/
     % git remote add origin https://github.com/shinobee/MyGpsMap.git
    
  2. GitHub と同期

    • 「ターミナル」にて以下のように、コマンドを実行
      • 【なぜ?】
        • GtHubでリポジトリを作成したあとに表示された「Quick setup」に書かれていたから
        • このコマンドを実行することで、GitHubのリポジトリと同期(今回はpush≒アップロード)できる
        • なお、この時点では全てのファイルが同期(push)されていない
        • 今までの記事では、ソースコード管理をしてこなかったので、Xcodeが自動作成した初期状態のファイルが同期(push)対象
        • これ↑が理解できない人は、他の人の記事をもとにGitについて勉強しましょう
    console
     % git push -u origin master
    
    • なお、以下のようなコマンド実行結果が出力(表示)された
    console
    Enumerating objects: 33, done.
    Counting objects: 100% (33/33), done.
    Delta compression using up to 4 threads
    Compressing objects: 100% (29/29), done.
    Writing objects: 100% (33/33), 11.30 KiB | 2.82 MiB/s, done.
    Total 33 (delta 3), reused 0 (delta 0)
    remote: Resolving deltas: 100% (3/3), done.
    To https://github.com/shinobee/MyGpsMap.git
    * [new branch]      master -> master
    Branch 'master' set up to track remote branch 'master' from 'origin'.
    
    • ブラウザでgithubのリポジトリを表示したら以下のような状態
      github-init.png
  3. プロジェクトフォルダ以下の全てのファイルを git で管理

    • 「ターミナル」にて以下のように、コマンドを実行

      • 【なぜ?】
        • プロジェクトに関連する全てのファイルを対象とするため
        • git のコマンドオプション add で管理対象ファイルを追加することを指示
        • git のコマンドオプション '.' ドットで、カレント(現在の)フォルダ以下全てを指定
      console
         % cd $HOME/github/shinobee/MyGpsMap/
         % git add .
      
  4. プロジェクトフォルダ以下の全てのファイルを git で管理

    • 「ターミナル」にて以下のように、コマンドを実行

      • 【なぜ?】
      • commit で管理対象ファイルを確定させることを指示
      • -m オプションで、コメントを記録
      console
       % cd $HOME/github/shinobee/MyGpsMap/
       % git commit -m 'update MyGpsMap project'
      
  5. git の内容をGitHubと同期

    • 「ターミナル」にて以下のように、コマンドを実行

      • 【なぜ?】
        • Mac上での変更内容をGitHubに登録するため
      console
      % git push 
      Enumerating objects: 35, done.
      Counting objects: 100% (35/35), done.
      Delta compression using up to 4 threads
      Compressing objects: 100% (24/24), done.
      Writing objects: 100% (24/24), 126.10 KiB | 21.02 MiB/s, done.
      Total 24 (delta 5), reused 0 (delta 0)
      remote: Resolving deltas: 100% (5/5), completed with 5 local objects.
      To https://github.com/shinobee/MyGpsMap.git
      091844c..c7f6b12  master -> master
      
    • ブラウザでgithubのリポジトリを再表示(reload)したら以下のような状態

      github-pushed.png

今回の到達点

  • プロジェクト(MyGpsMap)がGitHubにて公開された状態になった

さいごに

  • このシリーズの最初に書いた「+αの目標」は達成できたのでしょうか?(自問)
    • 説明を書き始めると、どこまで深く説明して良いのかわからなくなる
    • 軽く読み切れる記事のボリュームを考えて説明を書いていたので、説明不足感があったかもしれません
  • その中で、できるだけ説明を書いたつもりだけど、余計にわかり難くなったとしたら、ごめんなさい
+αの目標[再掲]
世の中に「やってみました」記事は、たくさんある。
多くの場合、同じことをすれば同じ結果を得ることができる。
しかし「なぜそのようなことをするのか?」を
解決できないから自分が成長しない。
『人が書いた「やってみました」記事を参考に
できるだけ「なぜ?」を解決しながらやってみよう』(汗)

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1:プロジェクト作成)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2:地図表示)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3:位置情報取得)
  4. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4:位置情報と連携した地図表示)
  5. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5:アプリアイコン設定)
  6. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その6:拡大・縮小ボタン追加)
  7. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その7:地図を拡大・縮小)
  8. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その8:地名表示)
  9. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その9:ソースコード管理)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

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

Proxyパターン概要

Proxy(プロキシ、代理人)とは、大まかに言えば、別の物のインタフェースとして機能するクラスである。
その「別の物」とは何でもよく、ネットワーク接続だったり、メモリ上の大きなオブジェクトだったり、複製がコスト高あるいは不可能な何らかのリソースなどである。

引用:Wikipedia

さらに細分化した呼称および概要

1. Virtual Proxy
・コストのかかるオブジェクトの生成を代理して、メモリ使用量を削減したり処理時間を短縮します。
・Flyweightパターンを加えることが多いと思われます。Flyweightについては別記事をご参照ください。
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Flyweight~

2. Protection Proxy
・オブジェクトごとに異なるアクセス権が必要なとき、アクセス制御を代理します。

3. Remote Proxy
・別ロケーションや別アドレス空間にあるオブジェクトのローカルな代理を提供します。

使い所

Virtual Proxy / Remote Proxy

『サーバー上の画像をアプリ上で表示する時、一度取得した画像はアプリ内にキャッシュしておき、毎回サーバーにアクセスすることを避ける』というような例です。

・「メモリ使用量を削減したり処理時間を短縮する」という観点でみるとVirtual Proxy
・「別ロケーションや別アドレス空間にあるオブジェクトのローカルな代理を提供する」という観点でみるとRemote Proxy

Protection Proxy

iOSアプリのアクセス制御となると大抵はUIを伴うので、アクセス制御に適用というのは個人的にピンと来ないです…

少しひねって考えると、

APIのレスポンスをローカルDBに書き込むケースで、
1. 各項目の値の論理チェック行い、正当な値だった場合に
2. insert/updateを行う
という流れが良くあると思います。

このような時、論理チェックを別クラスに代理させるというパターンはProtection Proxyとして考えられるのかも知れません。
(とはいえ構造が複雑になると思うので、そのような設計を選択すべきケースは少ないかも知れませんが)

サンプルコード (Virtual Proxy)

Swiftバージョンは 5.1 です。

// Protocol
protocol ImageProvider {
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void)
}

// サーバーからImageを取得するクラス
class ImageRequest: ImageProvider {
    // サーバーからImageを取得する
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void) {
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            // エラー判定省略
            guard let data = data, let image = UIImage(data: data) else {
                print("Couldn't parse image.")
                completion(nil)
                return
            }
            completion(image)
        }.resume()
    }
}

// ローカルにImageをキャッシュするクラス
class ImageCache {
    // 画像キャッシュ
    static var imageCache = NSCache<AnyObject, AnyObject>()

    // キャッシュからImageを取得する
    class func searchImage(with urlString: String) -> UIImage? {
        return imageCache.object(forKey: urlString as AnyObject) as? UIImage
    }

    // キャッシュにImageを保存する
    class func saveImage(_ image: UIImage, for urlString: String) {
        imageCache.setObject(image, forKey: urlString as AnyObject)
    }
}

// Proxyクラス
class ImageProxy: ImageProvider {
    // Imageを(代理で)取得する
    func requestImage(with urlString: String, completion: @escaping (UIImage?) -> Void) {
        if let cacheImage = ImageCache.searchImage(with: urlString) {
            // キャッシュに存在する場合はキャッシュから取得する
            completion(cacheImage)
            return
        } else {
            // キャッシュに存在しない場合はサーバーから取得する
            ImageRequest().requestImage(with: urlString) { (image) in
                guard let image = image else {
                    completion(nil)
                    return
                }
                // 画像を取得できたらキャッシュに保存する
                ImageCache.saveImage(image, for: urlString)
                completion(image)
            }
        }
    }
}

// Usage
class ViewController: UIViewController {
// (省略)
    @IBAction func buttonTapped(_ sender: Any) {
        let imageView = UIImageView()
        let urlString = "https://upload.wikimedia.org/wikipedia/commons/5/56/Donald_Trump_official_portrait.jpg"
        ImageProxy().requestImage(with: urlString) { (image) in
            DispatchQueue.main.async {
                imageView.image = image
            }
        }
    }
}

サンプルコード (Protection Proxy)

// Protocol
protocol ModelUpdate {
    func write(with name: String)
}

// Subject
class Model: ModelUpdate {
    let id: Int
    var name = ""

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

    func write(with name: String) {
        self.name = name
        print("Model was writed.")
    }
}

// Proxy
class ProxyModel: ModelUpdate {
    private let model: Model

    init(id: Int, name: String) {
        self.model = Model(id: id, name: name)
    }

    func write(with name: String) {
        if name.isEmpty {
            print("Name is empty.")
            return
        }
        if name.contains(where: { !$0.isASCII }) {
            print("Name is incorrect.")
            return
        }
        model.write(with: name)
    }
}

// Usage
let model = ProxyModel(id: 100, name: "Taro")
model.write(with: "")   // Name is empty.
model.write(with: "?") // Name is incorrect.
model.write(with: "Hanako") // Model was writed.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

前書き

GoFのデザインパターンが世に登場した『オブジェクト指向における再利用のためのデザインパターン』は1995年の発行だそうで、もはや古典の域ですね。

Swiftなどのモダンな言語では「もう使わないパターン」とか「もっとシンプルに解決できるパターン」などもあり、具体的な解決方法として崇め奉るモノではないかなと思います。

しかし、GoFのデザインパターンを、「解決方法」ではなくて、開発で良く陥る「状況」と解決に至る「発想」、そしてそれに対する「名前付け」に着目すると、改めて研究してみる価値はあると考えます。

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

記事の一覧

生成に関するパターン 概要
Factory Method 実際に生成されるインスタンスに依存しない、インスタンスの生成方法を提供する。
Abstract Factory 関連する一連のインスタンスを状況に応じて、適切に生成する方法を提供する。
Builder 複合化されたインスタンスの生成過程を隠蔽する。
Singleton あるクラスについて、インスタンスが単一であることを保証する。
Prototype 同様のインスタンスを生成するために、原型のインスタンスを複製する。
構造に関するパターン 概要
Adapter 元々関連性のない2つのクラスを接続するクラスを作る。
Facade 複数のサブシステムの窓口となる共通のインタフェースを提供する。
Bridge クラスなどの実装と、呼出し側の間の橋渡しをするクラスを用意し、実装を隠蔽する。
Composite 再帰的な構造を表現する。
Decorator あるインスタンスに対し、動的に付加機能を追加する。
Flyweight 多数のインスタンスを共有し、インスタンスの構築のための負荷を減らす。
Proxy 共通のインタフェースを持つインスタンスを内包し、利用者からのアクセスを代理する。
振る舞いに関するパターン 概要
Chain of Responsibility イベントの送受信を行う複数のオブジェクトを鎖状につなぎ、それらの間をイベントが渡されてゆくようにする。
Command 複数の異なる操作について、それぞれに対応するオブジェクトを用意し、オブジェクトを切り替えることで、操作の切替えを実現する。
Mediator (執筆予定) オブジェクト間の相互作用を仲介するオブジェクトを定義し、オブジェクト間の結合度を低くする。
Memento (執筆予定) データ構造に対する一連の操作のそれぞれを記録しておき、以前の状態の復帰または操作の再現が行えるようにする。
Observer (執筆予定) インスタンスの変化を他のインスタンスから監視できるようにする。
State (執筆予定) オブジェクトの状態を変化させることで、処理内容を変えられるようにする。
Strategy (執筆予定) データ構造に対して適用する一連のアルゴリズムをカプセル化し、アルゴリズムの切替えを容易にする。
Visitor (執筆予定) データ構造を保持するクラスと、それに対して処理を行うクラスを分離する。
【以下は割愛】
Interpreter 普通のiOSアプリ開発では利用ケースが限定的なので割愛
Iterator Swiftでは言語機能でサポートされているので割愛
Template Method Swiftではprotocolとして言語仕様に組み込まれているため割愛

※概要はWikipediaより引用

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

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

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

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

Factory Methodパターン概要

  • 生成するインスタンスを条件に応じて柔軟に切り替えるためのパターンです。
  • Javaなどの場合は抽象クラスで振る舞いとインスタンス生成の雛形を定義し、サブクラスでそれらをオーバーライドします。(参照:Wikipedia
  • Swiftの場合は抽象クラスという概念がありませんし、enumの表現力が強力なので、enumをうまく使うと良さそうです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

アプリ開発の実務で適用できるケースはたくさんあると思いますが、例えば

  • 料金体系の違う「通常会員」と「優待会員」のオブジェクト生成を切り替える、とか
  • テスト用targetの場合はモックオブジェクトを生成する、とか

サンプルコード

Swiftバージョンは 5.1 です。

protocol CurrencyDescribing {
    var symbol: String { get }
    var code: String { get }
}

final class Euro: CurrencyDescribing {
    var symbol: String {
        return "€"
    }

    var code: String {
        return "EUR"
    }
}

final class UnitedStatesDolar: CurrencyDescribing {
    var symbol: String {
        return "$"
    }

    var code: String {
        return "USD"
    }
}

enum Country {
    case unitedStates
    case spain
    case uk
    case greece
}

enum CurrencyFactory {
    static func currency(for country: Country) -> CurrencyDescribing? {
        switch country {
            case .spain, .greece:
                return Euro()
            case .unitedStates:
                return UnitedStatesDolar()
            default:
                return nil
        }
    }
}

// Usage
print(CurrencyFactory.currency(for: .greece)?.symbol ?? "")   // "€"
print(CurrencyFactory.currency(for: .spain)?.symbol ?? "")    // "€"
print(CurrencyFactory.currency(for: .unitedStates)?.symbol ?? "") // "$"

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-factory-method

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

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

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

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

Abstract Factoryパターン概要

  • オブジェクトの生成を抽象化することにより、関連/依存する複数のオブジェクト生成を一括して提供するためのパターンです。
  • Factory Methodと類似していますが、主な相違点は上記の太字部分です。
  • オブジェクト生成の責務を担う側を「抽象的な工場」に見立てて、Abstract Factoryと呼びます。
  • 利用側は「工場」に対して生成条件を提示するわけですが、Swiftの場合はenumを上手く使うと良さそうです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

  • テスト時はDBの処理をモック化するとか
  • (iOSアプリではあまりないと思いますが)マルチデータベース対応 とか

サンプルコード

Swiftバージョンは 5.1 です。

// Protocols
protocol DatabaseConnection {
    var connection: String { get }
}

protocol DatabaseAccessible {
    func select()
    func insert()
    func delete()
    func update()
}

protocol DatabaseProvider {
    func makeConnection() -> DatabaseConnection
    func makeAccessor() -> DatabaseAccessible
}

// Class
final class MockConnection: DatabaseConnection {
    let connection = "MockConnection"
}

final class MockAccessor: DatabaseAccessible {
    func select() { print("ダミーを返す") }
    func insert() { print("何もしない") }
    func delete() { print("何もしない") }
    func update() { print("何もしない") }
}

final class ProductionConnection: DatabaseConnection {
    let connection = "ProductionConnection"
}

final class ProductionAccessor: DatabaseAccessible {
    func select() { print("実際に読み込む") }
    func insert() { print("実際に追加する") }
    func delete() { print("実際に削除する") }
    func update() { print("実際に更新する") }
}

// Abstract factory
enum DatabaseFactoryType: DatabaseProvider {
    case mock
    case production

    func makeConnection() -> DatabaseConnection {
        switch self {
        case .mock:
            return MockConnection()
        case .production:
            return ProductionConnection()
        }
    }

    func makeAccessor() -> DatabaseAccessible {
        switch self {
        case .mock:
            return MockAccessor()
        case .production:
            return ProductionAccessor()
        }
    }
}

// Usage
let mockFactory = DatabaseFactoryType.mock
let mockConnection = mockFactory.makeConnection()
print(mockConnection.connection)   // "MockConnection"
let mockAccessor = mockFactory.makeAccessor()
mockAccessor.select()  // "ダミーを返す"

let productionFactory = DatabaseFactoryType.production
let productionConnection = productionFactory.makeConnection()
print(productionConnection.connection)   // "ProductionConnection"
let productionAccessor = productionFactory.makeAccessor()
productionAccessor.select()  // "実際に読み込む"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む