- 投稿日:2020-01-13T23:14:46+09:00
#19 AdMobを使う場合のアプリの申請1例
- 投稿日:2020-01-13T21:59:28+09:00
#18 ホーム画面に表示するアプリ名を変更する1例
- 投稿日:2020-01-13T19:36:48+09:00
テストのための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の注入によって選択できる(機能を差し替えられる)ことになります。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:クラス図
在庫一覧クラスと各管理クラス抽象の間に引かれた線は、PersonクラスとSpeakableの間では、ひし形の塗りつぶしであった記号と線(
composition
)でしたが、こちらではただの実践のみです。
この線のことをUMLではassociation
と呼び、各クラスとは密結合せず5、単純なメソッド呼び出しなどで関連していることを意味しています。当然ライフサイクルも異なります。さらに各管理クラスはここには記述がない他のクラスのために情報の追加や、削除、更新機能を有しています。
DIの目的の違い
上記の2つの例は、密結合、疎結合という違いがありましたが、実は目的も異なっています。
それは依存を注入される側の機能を変更したいかどうかということです。依存が密結合なDI
の例では、もともとの目的が機能の切り替えのためのDIを採用したということになりますので必須です。切り替えたくないのなら、そもそも分離することは無意味です。
依存が疎結合なDI
の例では、実は機能を切り替えることはありません。もちろんそのような要件があってDIを用いることが必要な場合もあるでしょう。しかし、今回提示した例では現在のところDIが必要なようには見えません。実は別に目的があるといことです。下のクラス図を見てください。
この新しいクラス図では、各管理クラスの代わりにテスト用スタブに切り替えることができるようになっています。つまりDIの目的はテストであるということです。
何が問題なのか
みなさんは、機能要件を実現するための設計と、テストを目的にした設計を区別せずに、たまたま同じ設計でできるからと言う理由で、実施しまうのに違和感がありませんか?
私には後者の例においてはいくつかの問題点があると考えています。不要な機能の注入
まず問題点としてあげられるのは、不要な機能が注入されているということです。
在庫一覧クラスは、注入されたクラスの一部の機能しか必要としていません。
在庫一覧で必要なのは各管理クラスから情報が取得できれば良いのであって、追加、更新、削除のメソッドは必要ありません。もしテストしないのならばこれらのメソッドは呼び出せるけど使わないだけで終わってしまいますが、テストを考えるとそうはいきません。テストのために不必要なメソッドまで(たとえ中身が空であっても)実装する必要があります。これはクラスの規模が大きくなってくると無視できないほどの負荷になるでしょう。
目的と注入されたオブジェクトのメソッド名が一致しない(ことがある)
またテスト以外でも問題はあります。お互いに独立して外部にメソッドを公開しているために、呼び出す側の目的と、実際に呼びださなければならないメソッド名称の乖離が発生します。このことは、ソースコードを読解する上ではすくなからぬ障害になると考えます。
- 呼び出す側の目的: 会員番号を利用したい
- 呼び出すメソッド名: fetchData()
例があまり良くないですが、言わんとすることは分かっていただけるのではないかと思います。
自分自身のメソッドであれば、適切な名前を付けることも可能ですが、独立した外部のクラスのメソッド名はまったく別の理由で名付けられていることもあります。特に設計者・実装者が他人の場合は(ましてやスキルレベルや開発会社が異なっていたりすると)頻繁に遭遇するかもしれません。依存が散逸する
依存した外部のプロトコルによる実装を直接呼び出すことで、依存が散逸します。
在庫一覧クラスの例で言えば、依存は変数product, user, shopに格納され在庫一覧
クラスの各所で呼びだされたりします。
依存がproduct1つだけであればproductを検索すれば良いだけですが、依存が複数になりさらに上記のメソッド名が一致しない問題と相まってソースコードの読解はより難しくなっていくでしょう。改善案
UML:クラス図
依存が密結合なDI
と同様に在庫一覧はcompositionの関係にある在庫一覧DependencyProtocolを持ちます。この在庫一覧DependencyProtocolには、在庫一覧が依存するメソッドしか記述されていません。
このようにすることで、不要な機能の注入を避けることができます。また目的と注入されたオブジェクトのメソッド名が一致しない問題も回避することができます。在庫一覧DependencyProtocolのメソッド名は在庫一覧クラスが自由に名付けられるからです。例えばこの例では、商品一覧取得であったものを在庫一覧取得などのように付け替えています。
依存が散逸する問題も在庫一覧DependencyProtocolに依存が集められたことで、在庫一覧が外部に依存するすべての項目が一覧できるようになります。
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:クラス図
この手法にも何点かの問題があります。
その一つが、不要な機能の注入です。あれっと思われると思いますが、この場合の不要なはテストにとってと言うことです。
今回の例はシンプルすぎて発生しませんが、例えば在庫一覧
クラスの特定のメソッドをテストするときに、在庫一覧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
- 投稿日:2020-01-13T19:06:48+09:00
[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 キーの取得
- こちらから利用登録をする
- ログイン後、Developer API Keys を選択
- Create API Key を選択
- Name、Description、Country を入力し、API Key を作成
- 以下の画像の矢印で示した所に表示されているものが API Key になります
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.swiftimport 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 を使ってみた
今回は、
- 現在地の 3words を確認
- 検索欄から 3words を検索して位置を確認
の2つの機能を試してみようと思います
1. 現在地の 3words を確認
現在地の 3words を確認するために、まずは Google Maps SDK for iOS を使用し、
現在地の取得、表示を行いますGoogle Maps SDK for iOS を使用して現在地を地図上に表示にするまでの実装は別記事でまとめようと思います
ViewController.swiftimport 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つの単語は . で区切る必要があるようなので、
検索バーでは入力しやすいよう、スペース区切りで入力してもらって、それを . 区切りに変換するような処理を行なっていますViewControllerclass 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.swiftimport 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.swiftlet GOOGLE_MAPS_API_KEY = "{Your API key}"参考
- 投稿日:2020-01-13T18:29:57+09:00
【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上で表示されているプロパティ名をかけばいい。
テーマカラーとして設定するには、todayColorとtitleColorとweekdayTextColorを変えればいいかな。
その三つをUIColor.systemPurple
に設定したのが、以下まとめ
記事を色々探すより、ソースコードをきちんと読むの、だいじ。
参考文献
- 投稿日:2020-01-13T18:29:57+09:00
【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上で表示されているプロパティ名をかけばいい。
テーマカラーとして設定するには、todayColorとtitleColorとweekdayTextColorを変えればいいかな。
その三つをUIColor.systemPurple
に設定したのが、以下まとめ
記事を色々探すより、ソースコードをきちんと読むの、だいじ。
参考文献
- 投稿日:2020-01-13T17:54:15+09:00
レスポンスの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.swiftstruct 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.swiftlet 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で教えていただきました。
こういうかんじですー pic.twitter.com/LFy4BXuaMI
— takasek (@takasek) January 12, 2020こちらを元に、上記のコードを改善します。
…と思ったのですが、 自分の実力ではすぐにはできませんでした 。
途中まで記述したコードを載せますが、見当違いのことをしているかもしれません。ExchangeRatesDTO.swiftstruct 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
おわりに
行き詰まったので一旦ここまでにします。
わかる人がいたら教えていただけると嬉しいです?
- 投稿日:2020-01-13T16:07:16+09:00
@_functionBuilderを理解してSwiftUIイリュージョンのタネをあかす
はじめに
最近ようやく少しずつ
SwiftUI
に触れてみようかなと思い始めました。
SwiftUI
とは、宣言的なUI実装を実現した、
近い将来iOSのUI描画のメインストリームにもなるであろうフレームワークです。まずはこれを見てください。
画像のソース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
属性のViewBuilder
のbuildBlock
メソッドの引数に渡すことで、
VStack
が子ViewのText("hoge")
を生成した、というのがイリュージョンのタネです。もう少し踏み込んでみましょう。
今回の例に沿えば、@_functionBuilder
の仕組みは以下のように説明できます。1.
ViewBuilder
に@_functionBuilder
属性を付ける(→@ViewBuilder
属性が使えるようになる)
2.VStack
のinit
のクロージャー引数content
に@ViewBuilder
属性を付ける
3.そのクロージャーに渡すText("hoge")
がViewBuilder
のbuildBlock
メソッドに渡されるようになるちなみに、複数の子Viewを生成するケースは
ViewBuilder
のextension
に定義されています。
子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
属性のViewBuilder
のbuildBlock
メソッド
によって成り立っていました。これに加えてクロージャーの
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であるとわかりました。
- 投稿日:2020-01-13T15:18:09+09:00
テスト時のみEquatableプロトコルに準拠させる方法(Swift)
はじめに
「製品コードでは構造体を比較しないけど、テスト時は比較したい」ということはありませんか
テストで比較するためだけにEqutableに準拠させるのはありかなぁ?
— ウホーイ (@the_uhooi) January 12, 2020
Equatable
プロトコルに準拠することで、構造体同士を==
で比較できるようになります。FooEntity.swiftstruct FooEntity: Equatable { let name: String let age: Int }FooTests.swiftlet 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 さんから教えていただきました。
#if TEST
— はるふ(冬) (@_ha1f) January 13, 2020
extension Foo: Equatable {}#endif
はどう思いますか
(僕の中ではなしよりのあり)テスト用のマクロを作り、テスト時のみエクステンションで準拠させます。
FooEntity.swiftstruct 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でご連絡ください!
- 投稿日:2020-01-13T14:20:19+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その9)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の9回目です。
今回は、いままでやってきたことを git でソースコード管理し、GitHub にて公開までします。
とりあえず、今回のシリーズは、これで終了の予定。懺悔(ざんげ)
- 今回のシリーズは、アプリを「作ってみる」ことが主眼だったので git に関することは「あとまわし」だったけど、本当であれば「最初」にすることだと思います
- そのようなこともあり、今回の記事は、あとからgitでソースコード管理し、GitHubと連携することになります
- 一部の方には参考になると思いますが、多くの方には参考になりません
GitHubアカウントの作成
- ブラウザで、ここ↓にアクセス
- https://github.com/join
- 手順などの詳細は、他の記事を参照してください
- 簡単に作成できるはずです
ソースコード管理対象の確認
- 【なぜ?】
- 権利(著作権など)の侵害は、法律違反
- 違反すると罰せられる
- 他人のものを、自分のもののように公開するのは、格好が悪くない?
手順
- 「ターミナル」アプリを起動
- 「ターミナル」にて以下のように、プロジェクトフォルダ全体に対し検索
- 【なぜ?】
- cd で地図アプリ(MyGpsMap)のプロジェクトフォルダへ移動している
- プロジェクトフォルダの位置に関しては、この記事を参照
- grep コマンドは、テキストファイル内の文字列を検索するツール
- -r オプションは、サブフォルダ全てを検索対象と指定している
- -i オプションは、大文字小文字を区別せず検索することを指定している
- 'copyright' は、検索文字列を指定している
- 最後の * は、全てのファイルが対象であることを指定している
console% cd $HOME/github/shinobee/MyGpsMap/ % grep -r -i 'copyright' *
- 実行結果は、以下の通り
- 他者(shinobee以外)が copyright を持っているファイルが見つからない
- プロジェクトフォルダ全体を公開しても大丈夫
- プロジェクトフォルダ全体を公開してくれた方が、使う(git cloneする)人もファイルの過不足が気にならないので「楽」だと思います
consoleMyGpsMap/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リポジトリの作成
GitHubにログイン
- ブラウザで https://github.com/login を表示
- 【なぜ?】
- OSS(Open Source Software)のプロジェクトは、GitHubで公開で間違いないから
- GitHubでのプロジェクト作成は、ブラウザを使うから
- 自分は、他の手段を知らない
GitHubにリポジトリを新規作成
- 画面右上の「+」をクリックし、「New repository」を選択
- 以下のようにレポジトリ情報を入力
- [Create repository]ボタンをクリックして、しばらく待つ
XcocdプロジェクトとGitHubリポジトリの連携
GitHubアドレスを登録
- 「ターミナル」にて以下のように、コマンドを実行
- 【なぜ?】
- GtHubでリポジトリを作成したあとに表示された「Quick setup」に書かれていたから
- このコマンドを実行することで、GitHubのリポジトリを連携づけできる
- 『プロジェクトに教えてあげる』と考えるほうが理解しやすいかも
console% cd $HOME/github/shinobee/MyGpsMap/ % git remote add origin https://github.com/shinobee/MyGpsMap.gitGitHub と同期
- 「ターミナル」にて以下のように、コマンドを実行
- 【なぜ?】
- GtHubでリポジトリを作成したあとに表示された「Quick setup」に書かれていたから
- このコマンドを実行することで、GitHubのリポジトリと同期(今回はpush≒アップロード)できる
- なお、この時点では全てのファイルが同期(push)されていない
- 今までの記事では、ソースコード管理をしてこなかったので、Xcodeが自動作成した初期状態のファイルが同期(push)対象
- これ↑が理解できない人は、他の人の記事をもとにGitについて勉強しましょう
console% git push -u origin master
- なお、以下のようなコマンド実行結果が出力(表示)された
consoleEnumerating 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'.プロジェクトフォルダ以下の全てのファイルを git で管理
「ターミナル」にて以下のように、コマンドを実行
- 【なぜ?】
- プロジェクトに関連する全てのファイルを対象とするため
- git のコマンドオプション add で管理対象ファイルを追加することを指示
- git のコマンドオプション '.' ドットで、カレント(現在の)フォルダ以下全てを指定
console% cd $HOME/github/shinobee/MyGpsMap/ % git add .プロジェクトフォルダ以下の全てのファイルを git で管理
「ターミナル」にて以下のように、コマンドを実行
- 【なぜ?】
- commit で管理対象ファイルを確定させることを指示
- -m オプションで、コメントを記録
console% cd $HOME/github/shinobee/MyGpsMap/ % git commit -m 'update MyGpsMap project'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今回の到達点
- プロジェクト(MyGpsMap)がGitHubにて公開された状態になった
さいごに
- このシリーズの最初に書いた「+αの目標」は達成できたのでしょうか?(自問)
- 説明を書き始めると、どこまで深く説明して良いのかわからなくなる
- 軽く読み切れる記事のボリュームを考えて説明を書いていたので、説明不足感があったかもしれません
- その中で、できるだけ説明を書いたつもりだけど、余計にわかり難くなったとしたら、ごめんなさい
+αの目標[再掲]世の中に「やってみました」記事は、たくさんある。 多くの場合、同じことをすれば同じ結果を得ることができる。 しかし「なぜそのようなことをするのか?」を 解決できないから自分が成長しない。 『人が書いた「やってみました」記事を参考に できるだけ「なぜ?」を解決しながらやってみよう』(汗)連載
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1:プロジェクト作成)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2:地図表示)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3:位置情報取得)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4:位置情報と連携した地図表示)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5:アプリアイコン設定)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その6:拡大・縮小ボタン追加)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その7:地図を拡大・縮小)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その8:地名表示)
- [はじめてのiOSアプリ]xcodeで地図アプリを作成(その9:ソースコード管理)
- 投稿日:2020-01-13T13:23:43+09:00
[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 ProxyProtection 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.
- 投稿日:2020-01-13T09:52:45+09:00
[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より引用
- 投稿日:2020-01-13T09:48:51+09:00
[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
- 投稿日:2020-01-13T09:48:37+09:00
[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() // "実際に読み込む"