20191128のiOSに関する記事は10件です。

Xcodeの複数分割状態で定義を開くウィンドウを選ぶ

Xcodeで定義にジャンプしたりウィンドウを分割して開くなど機能があり、今回複数分割に対応したことで、これらの機能がどうなったかが分かったので記事にしました。

分割して開く

option+cmd+クリック で分割先がなければ用意し、あればそこで開きます。

閉じるときはウィンドウ左上の☓ボタンで閉じれます。

dest.gif

更に分割を増やす

Xcodeで待ち焦がれた複数分割をサポートされました。

これはウィンドウ右上のSplitedWindowの見た目に+と入ったアイコンを押すことで増やせます。(下図参照)
image.png

またこのアイコン上でoption を押しながらタップすることで分割先を変えることができます。(下図参照)
image.png

次の映像は縦分割→横分割をしています。
dest.gif

これを使えばたくさんのウィンドウを一度に開いた状態で開発できます。

image.png

複数画面の定義にジャンプで躓く :sob:

画面が複数ある状態で option+cmd+クリックしても期待する動きになりません。
これは内部的に分割した順番を持っており、次の分割先で開いています。

複数画面の定義にジャンプで開く先を選ぶ :innocent:

shift+option+cmd+クリック になります。 shiftを追加して定義ジャンプを試みると開くウィンドウを選べるようになります。

dest.gif

それでは良いXcode開発ライフを。

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

NFC と iOS13 でホームオートメーション

iOS13 から、NFCタグをつかってホームオートメーションを設定できるようになりました。

やったこと

NFCタグに iPhone を当てたら、電気がつく or 電気が消えるようにしました。

参考記事

iPhoneをかざすだけでオートメーション発動 NFCタグとショートカットで作業効率が爆上がり

必要なもの

  • iOS13 がインストールされた、iPhone -- iPhone 11, XS, XR
  • NFCタグ
  • 壁にNFCタグを貼るなら養生テープ
  • ショートカットから操作できるもの

手順

  1. iPhone でショートカットアプリを開く
  2. オートメーションタブをタップ
  3. 右上の「+」アイコンをタップ
  4. 「個人用オートメーションを作成」をタップ
  5. 「NFC」 をタップ
  6. 「NFCタグ スキャン」をタップ
  7. デバイスを、設定したいNFCタグにかざす
  8. タグに名前を付ける
  9. 「次へ」をタップ
  10. 「アクションを追加」
  11. 操作したいものを設定。複数可。
  12. 「次へ」をタップ
  13. 「実行の前に尋ねる」はオフにする
  14. 「完了」

あとは、NFCタグに iPhone をかざすとアクションが実行されるようになりますよ!

応用

アクションのところは、いろいろ応用可能です。
うちは照明やエアコンを操作したり
玄関のオートロック(オープンセサミ)を操作したりしてます。

あとがき

楽しい体験をできたので、記録を残すことにしました。
はじめて Qiita の記事を書きました。

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

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

はじめに

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

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

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

今回は、位置情報と連携した地図表示までします。

位置情報と連動した地図表示

  1. MapKirをインポート

    • 画面左側のファイルツリーから[ViewController.swift]を選択し、画面中央に表示されるエディタで、以下のように修正
    • 【なぜ?】
      • 地図ライブラリ(MapKit)を使うことを宣言することで、地図表示のプログラムを記述できるようになる
    ViewController.swift
    import UIKit
    import CoreLocation
    import MapKit  // この行を追加
    
    class ViewController: UIViewController, CLLocationManagerDelegate {
    
  2. MapKit用の変数(mapView)を追加

    • 同様に[ViewController.swift]を以下のように修正
    • 【なぜ?】
      • この変数を通してプログラムで地図位置を取り扱うため
    ViewController.swift
    class ViewController: UIViewController, CLLocationManagerDelegate {
        @IBOutlet var mapView: MKMapView!  // この行を追加
        var locationManager: CLLocationManager!
    
  3. MapViewにおいて現在位置を表示するように設定

    • 同様に[ViewController.swift]を以下のように修正
    • 【なぜ?】
      • 現在位置が表示された方が見やすいから
      • 試しにtureではなくfalseを設定してみたら、透明人間みたいに表示されないから面白いかも(笑)
    ViewController.swift
        locationManager.startUpdatingLocation()
        locationManager.requestWhenInUseAuthorization()
    
        mapView.showsUserLocation = true  // この行を追加
    }
    
  4. エディタ画面を2画面表示する

    • 画面左側のファイルツリーから[Main storyboard]を選択し、[Add Editor on Right]ボタンをクリックし、[Main storyboard]が2画面表示されるようにする
    • 【なぜ?】
      • この後の処理で[Main storyboard]に表示されている[MapView]を[ViewController.swift]に関連づけしやすくするため
        AddEditor.png
  5. [ViewController.swift]と[Main storyboard]を同時に表示する

    • 画面左側のファイルツリーから[ViewController.swift]を選択する
    • 【なぜ?】
      • [Main storyboard]と[ViewController.swift]が同時に表示されていると[MapView]の関連づけが容易にできるため
        EditViewControllerStoryboard.png
  6. MapKit用の変数(mapView)とMapViewとを関連づける

    • Ctrl+クリックで[MapView]を選択することでOutletを表示
      MapViewOutlet.png
    • [Referencing Outlets]の○印から、変数mapviewにドラッグ&ドロップで接続(connect)する
      ConnectOutlet.png
  7. 接続(connect)後の状態がこれ↓
    ConnectedOutlet.png

  8. テスト実行

    • Xcode 左上の矢印アイコンをクリック
    • Simulatorのメニューから[Debug]-[Location]-[City Run]など(移動するやつ)を選択
    • 地図を拡大表示すると、地図上を移動していることを確認できる

今回の到達点

  • Simulatorを使い、更新される位置情報に追従して地図上の位置(現在地)が移動するようになった

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
  4. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4)
  5. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift未経験者が1日でアプリを作った話

こんにちは、前回投稿から相当時間が空いているのでたぶん初投稿です。

なんかアプリを作ることになってしまったんですが1日でできてしまったのでそのことを書いてみます。

~前日譚~

えらいひと「iPhoneのGPSの精度わかんないし調べようよ」
ぼく「アプリつくるんですか」
えらいひと「つくっちゃお、Mac借りてくるから」
~数日後~
社内ITのひと「Mac持ってきました」
ぼく「は~い」(マジで作るんかSwiftやったことないぞ)

構成図

swift01.png

端末からGPSの座標を取得してAzure上にデプロイしたAPIへPOSTする単純なアプリです。

いざ開発

といいつつも日ごろから触ってない環境で触ったこともない言語で一寸先は闇。困ったときはGoogle先生に頼ります。

Xcodeを入れる

Xcode入れるとiOSアプリ作れるんやね。入れたる。

プロジェクトを作る

スクリーンショット 2019-11-28 17.19.39.png

Create a new Xcode project っと。

スクリーンショット 2019-11-28 17.21.08.png

なにこれ…とりあえず機能多くないし Single View App でいいか

スクリーンショット 2019-11-28 17.23.24.png

アプリの情報適当に入れてええか。Next 押して保存場所選択するとええな。

詰む

User InterfaceSwift UI になっててGoogleに聞けども聞けども噛み合いません。
天の神に聞いたら
Storyboard 選べ」
とのこと。

なるほど、Storyboard で作るとGoogleで出てくる情報通りにいくんだなと。

頼るべきは神。

User Interface を Storyboard にして再度プロジェクト作成

ぼく「Main.storyboardがある!!!!!!!!!」

スクリーンショット 2019-11-28 17.30.47.png

Main.storyboard を選択するとデザイナーが出るのでUIのパーツを置いていきます。

スクリーンショット 2019-11-28 17.32.28.png

それっぽくなってきた。

コードを書く

天の声「Assistant Editorを開くとこのUIに紐づいたコードが表示されるらしい。」

なるほど。

~10分後~

いや。丸いアイコンのボタンないじゃん。

swift02.png

天の声「どうやらXcodeのバージョンが新しくなっていてUIが変わってるらしい。」

スクリーンショット 2019-11-28 17.40.53.png

あった。
スクリーンショット 2019-11-28 17.56.52.png

viewDidLoad が起動時に走る感じっぽい?
とりあえず書いていきます。

位置情報を扱うにはまずimportを追加する必要があるとな。

import CoreLocation

次に位置情報を司る CLLocationManager を初期化する必要があると。
コンストラクタに書くのも長くなるしメソッド化します。

var locationManager : CLLocationManager!

func setupLocationManager() {
    locationManager = CLLocationManager()
    guard let locationManager = locationManager
        else { return }

    // アプリがバックグラウンドでも位置情報を取りたいのでAlwaysAuthorizationを要求
    locationManager.requestAlwaysAuthorization()

    // 端末で位置情報取得が許可されているか取得
    let status = CLLocationManaager.authorizationStatus()

    if status == .authorizedAlways {
        locationManager.delegate = self
        locationManager.distanceFilter = 2                                    // 位置情報を再取得する閾値(m)
        locationManager.activityType = CLActivityType.automotiveNavigation    // 車での移動を想定
        locationManager.allowsBackgroundLocationUpdates = true                // バックグラウンド取得の許可
    }
}

こいつを起動時に呼び出してあげましょう

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

そして位置情報更新時に走る処理も作成

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    let location = locations.first
}

あとはInfo.plistに位置情報要求時の文言を追記。

Keyに NSLocationAlwaysUsageDescription Valueに表示したい文言を追加っと。

スクリーンショット 2019-11-28 18.10.46.png

実機で見てみますか
image.png

:ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok: :ok:

あとバックグラウンドでの位置情報更新を許可しないといけなかった。
プロジェクトのSigning & Capabilitiesから :heavy_check_mark: を入れましょう。

スクリーンショット 2019-11-28 18.17.58.png

GPSは取れたからあとはAzureに投げるだけやな!
ということで投げる部分を書きます。

func sendGeoCoordinateToWebApi(_ geo: CLLocation) {
        let url = "https://<Azure App Service名>.azurewebsites.net/api/GeoCoordinate"        
        let request = NSMutableURLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        // jsonを作成
        let params:[String:Any] = [                    
            "Name": textOutlet.text as String?,        
            "Latitude": geo.coordinate.latitude,       
            "Longitude": geo.coordinate.longitude,     
        ]                                              

        // POSTする
        do {                                           
            request.httpBody = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
            let task:URLSessionDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) -> Void in
                let resultData = String(data: data!, encoding: .utf8)!
                print(resultData)
            })
            task.resume()
        } catch {
            print("Error: \(error)")
            return
        }
    }

jsonを作成の部分では以下のようなjsonを作っています。作ったAPIと形式を合わせてるだけなので適当です。

{
    "Name": "ワイ",
    "Latitude": 111,
    "Longitude": 111,
}

textOutletはtextboxから値を引いてくるため設定したアウトレットです。
あとはこれを一情報更新時に発火するメソッドから呼び出すだけやな!

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    let location = locations.first
    sendGeoCoordinateToWebApi(location)
}

完成!

早速AzureのSQL DBの中を見てみます。

swift03.png

値が入ってる~~~~~~~~~~~!!!!!!!!!!!!!!!!!!!!!!

というわけで1日(1営業日)でアプリ(と値を受け取るAPI)ができちゃいました。
おつかれさまでした。

おわり。

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

UXデザイナーと連携する際に考慮したい事項

1ピクセルのデザインにこだわるUXデザイナーとの連携で、いろいろ苦労した経験からの知見を簡易的にまとめます

ピクセル単位での調整を可能にする

エンジニアが、都度、確認できるデザインスペックが必要
XD、Sketch、Prott、Abstractなど、ピクセル単位で確認でき、素材を書き出せるツール・サービスを使う

要件定義書

最初、エクセルやスプレッドシートを使っていたが、エンジニアは更新しやすいものの、デザイナーや企画者は使いづらい
さらに、デザインスペックを別で用意する必要がある
→ XDで要件定義を作成してもらい、デザインスペックも同時に作成&確認できるようにした

ワイヤーフレーム

要件定義からデザインを起こすまでのステップで、デザイナーがどのレベルまでデザインに落とし込めばいいか分からない
→ アプリエンジニアは、要件とデザインの両方がないと、実現可能かを判断できないケースが多いので、ある程度のデザインは必要だが、完成版を見せられると、その時点で判断不能に陥ることが多いので、ワイヤーフレームレベルのデザインで十分。
デザイナーとエンジニアで話し合うステップが必要なので、ワイヤーフレームレベルのデザインで一旦、エンジニアに共有する

デザインスペックの指示通りに、修正は結構な手間がかかる

デザイナーがXcodeを使えるようにして、文字サイズやフォント、ピクセル単位での修正は、デザイナーにやってもらう

デザイナーから受け取った素材を、リネームする手間がかかる

スプレッドシートで、素材依頼シートを作って、名前を指定して書き出してもらう
※あまりいい運用じゃなかったので、もっといい方法を考えた方がいい

ピクセル単位で調整していると同じような素材が増え、アプリサイズが肥大化する

共通で使う素材は、命名規則で判別しやすいようにする。common_XXXXX など
デザイナーがエンジニアに渡す際に、共通で使う素材かどうかを伝えてもらえると助かります。

カラーが細かくなり、どのカラーを使えばいいか、迷ってしまう

基本は、デザインスペック通りだが、間違えてるのではと思う箇所が出がちなので、大まかに、テキスト用のカラーとか、テーマカラーとかを決めて、間違いに気づけるように

iOSのTips
- 行間は、TextFieldだと行間指定できないので、UILabelかTextViewに変更する必要がある
- フォントを、システムフォントじゃなくて、ヒラギノに明示的に設定する
AndroidのTips
- Android は、dpで指定 (端末のフォントサイズ設定に対応する場合は、spがいいが、フォントサイズ対応すると、レイアウト崩れを考慮しないといけないので、逆にdp指定で対応させない)

デザインのベースにする解像度の決め方

各プラットフォームでの端末シェアを考慮

【2019最新】スマホ・タブレットの解像度一覧表(画面サイズの割合)
https://webdesign-abc.com/tech/resolution-list/

【2019年】最新のディスプレイ・モニター解像度シェアが知りたい!
https://web-knight.net/display-resolution/

「iPhone6/7/8」で解像度は「375 * 667」
「Android XPERIA」。解像度は「360 x 640」
が無難

デザイナーにお願いしたいこと

Atomic Design を考慮して、パーツ・コンポーネント単位で定義してほしい→

これが出来ると、開発でもコンポーネント化しやすい
[参考]
Atomic Design を分かったつもりになる
https://design.dena.com/design/atomic-design-%E3%82%92%E5%88%86%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%A4%E3%82%82%E3%82%8A%E3%81%AB%E3%81%AA%E3%82%8B/

AppleのHIGを隅々まで熟読してほしい

Human Interface Guidelines
https://developer.apple.com/design/human-interface-guidelines/
HIGから学ぶ、3つのナビゲーションとそのあるべき姿とは
https://note.com/ta0o_o0821/n/n140cd75cc023

HIGを理解したあと、iOSとAndroidアプリの違いや、マテリアルデザインを熟読

マテリアル デザインでアプリの魅力を高める
https://developer.android.com/distribute/best-practices/develop/use-material-design?hl=JA
ただ、個人的には、マテリアルデザインのリファレンスは、より具体的な実装方法のマニュアル的な部分が多く記事も多いので、熟読するというより、マテリアルデザインがどういうもので、どんな事ができるかを把握してほしいという感じ
Human Interface Guidelines は概念的な内容で、マテリアルデザインはデザイン手法のマニュアルといった感じ

その他

iOSとAndroidのUIの違い
https://nogson2.hatenablog.com/entry/2018/11/16/123256

iOS、Androidの解像度について(iOSはpt / Androidはdp)
https://qiita.com/eKushida/items/ff1ecaacdb54ce7f9f5c

エンジニアもデザインのことを知る必要がある

【これからのスキル】デザイナーとエンジニアの境界線がどんどん無くなる
https://blog.btrax.com/jp/designer-engineer/
優れたプロダクト開発のために。エンジニアがデザインを学ぶ時のおすすめ本
https://goodpatch.com/blog/designbooks-for-developer/

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

no app store connect access for the teamの対処法

概要

archive後、validateやdistribute時に「no app store connect access for the team」と出てきて
苦戦したので対策を書いておきます。
知りたいのは結果だと思うので成功した対処法は先に書いておきます。

成功した対処法

  • 再起動。 PCかXcodeを再起動するとエラーが解決できたので、この問題にぶつかった時はぜひやってみてください。

試したこと

  • app storeの識別子、認証、プロファイルなど
  • archiveの右下にあるDetailsのTeamを入力し直す

参考(他の参考になりそうな投稿を載せておきます)

No App Store connect access for the team 対策
アプリをArchiveするときに No accounts with iTunes Connect access というエラーが出てアップロードできない時

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

iOSのいろいろなアーキテクチャパターンを試してみる

はじめに

iOSのアーキテクチャパターンに言及する記事はすでにQiita上のみならず、ネット上にたくさん存在しています。

それらの記事は大変わかりやすく、読んだ直後はわかった気になるのですが、学んだアーキテクチャをいざ実際に使おうとなると途端になにもできなくなり、ほとんど理解していなかったことに気づくのです…。

そこで今回は、iOSの設計パターンについてきちんと勉強しなおし、備忘録とするべく、その内容を自分なりの言葉と簡単なサンプルコードでまとめてみようと思います。

本記事で取り扱う設計パターン

  • MVC
  • MVP
  • MVVM
  • Flux

作ったものとソースコード

今回作るのはごく簡単なGitHubのクライアントアプリ。GitHub APIを使い検索ワードに応じてRepositoryを一覧表示します。TableViewのCellをタップするとSafariViewControllerを使って内容を詳細表示する仕様としました。

https://github.com/TatsuhiroAbe/iOSDesignPatterns
アーキテクチャごとにブランチを用意しています。

アーキテクチャパターンを学ぶ前に

勉強を進めていくなかで、「そもそもなぜアーキテクチャパターンを開発したり利用したりするのか」をきちんと認識することがかなり重要だと感じました。

一言で言ってしまえば、責務を適切に分離するためということになるでしょうか。
ソフトウェア開発を行なっていると、開発が進むにつれて機能が増えていき、対処すべき問題がどんどん大きくなってしまいます。そこで、それらの問題をより小さな単位に分離して、出来るだけ単一の責務に向かわせようとするのが一般的ですよね。

アーキテクチャパターンというのは、その責務の切り分けを適切に行うための、文字通り「パターン」です。対処すべき問題はソフトウェアごとに違えど、その本質的な部分は多くの場合共通しています。そのような多くの場面で直面しうる責務の切り分け方をパターン化してくれたのが「アーキテクチャパターン」です。

特にiOSのアーキテクチャパターンにおいては、Presentation Domain Separation(PDS)というアイデアが基本になっているようです。

Presentationとは「UIに関するロジック」であり、Domain「システム本来の関心領域」を指します。これらはそれぞれMV*系のアーキテクチャにおけるViewとModelに相当する部分ですね。

私も含め「結局よく分からない」という人は、「責務を適切に分離するため」、特に「UIに関するロジックシステム本来のロジックを分離するため」という目的を意識しながらそれぞれのアーキテクチャパターンに向き合うと、やや理解が容易になるのかもしれません。

MVC

まず最初は、今回取り上げるアーキテクチャの中でもおそらく一番有名なMVC。iOSアプリに限らず、多くのWebフレームワークでも採用されているアーキテクチャです。

iOSアプリで利用されるMVCは厳密にはCocoa MVCと呼ぶそうで、よく下のような図とともに説明されています。


(https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.htmlから引用)

前述のPDSによると、View(UIに関するロジック)とModel(ビジネスロジック)を分離することが基本的な目的になります。MVCにおいてはControllerがViewとModelを参照し、両者の仲介役となることでそれを実現します。

このような設計であることから、Controllerの部分はどうしてもいろいろな処理が集中しがちで、いわゆるFarViewControllerになりやすいです。「MVCはMassive View Controllerのことだ」という皮肉交じりの表現はかなり有名ですね。

実装上のポイントは、Modelが自らの状態更新をControllerへ通知する部分(図中の"Notify"の部分)だと思います。イベント通知の手段として、SwiftではDelegate、クロージャ、NotificationCenterなどを使うことが多いですが、今回はDelegateパターンによって実装しました。

RepositoryModel.swift
protocol RepositoryModelDelegate: class {
    func repositoryModel(_ repositoryModel: RepositoryModel, didChange repositories: [Repository])
}

class RepositoryModel {

    let BASE_URL = "https://api.github.com/search/repositories?q="

    weak var delegate: RepositoryModelDelegate?

    private(set) var repositories: [Repository] = [] {
        didSet {
            delegate?.repositoryModel(self, didChange: repositories)
        }
    }

    func fetchRepositories(_ query: String) {
        let url = URL(string: BASE_URL + query)!
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else { return }
            if let error = error {
                print("error: \(error.localizedDescription)")
                return
            }

            do {
                let repositoriesList = try JSONDecoder().decode(RepositoriesList.self, from: data)
                self.repositories = repositoriesList.repositories
            } catch let err {
                print("decode error: \(err.localizedDescription)")
                return
            }

        }
        task.resume()
    }
}

MVP

MVPはModel、View、Presenterという3つのレイヤーから成るアーキテクチャです。それぞれの役割はこんな感じ

  • Model:他のアーキテクチャにおけるModelと同じ。ビジネスロジックを担うレイヤー。
  • ViewViewControllerも含めたUIViewのサブクラス。UIイベントを受け付け、Presenterに伝える。
  • Presenter:ViewとModelの仲介役。Viewからの入力を受け、Modelにコマンドを送る。Modelからの状態更新を受け取り、Viewを更新。Viewに表示するためのデータを保持するのもここ。

ViewとModelを完全に分離したいという目的はMVCと共通ですが、MVPではさらに描画処理とプレゼンテーションロジックを分離するという目的があります。

ここで言う「描画処理」というのは、label.text = newTexttableView.reloadData()という文字通り描画のための処理のことで、「プレゼンテーションロジック」というのは、ビューの更新やページ遷移(どの内容を表示するのかを決定する処理)などのUIのビジネスロジックを指します。

MVCではViewControllerにまとめらていたこれら2つの処理をViewとPresenterにそれぞれ分離することで、FatViewControllerを避けられるということです。

以下に示すのはサンプルのPresenterの実装です。

RepositoryViewPresenter.swift
protocol RepositoryPresenter: class {
    var view: RepositoryView? { get set }
    var numberOfRepositories: Int { get }
    func repository(_ row: Int) -> Repository?
    func didSelectRow(at indexPath: IndexPath)
    func didTapSearchButton(with searchText: String?)
}

class RepositoryViewPresenter: RepositoryPresenter {

    weak var view: RepositoryView?

    private(set) var repositories: [Repository] = []

    private var model: RepositoryModelProtocol!

    init(model: RepositoryModelProtocol = RepositoryModel()) {
        self.model = model
    }

    var numberOfRepositories: Int {
        return repositories.count
    }

    func repository(_ row: Int) -> Repository? {
        guard row < repositories.count else { return nil }
        return repositories[row]
    }

    func didSelectRow(at indexPath: IndexPath) {
        guard let repository = repository(indexPath.row) else { return }
        view?.showSafariView(repository.url)
    }

    func didTapSearchButton(with searchText: String?) {
        guard let searchText = searchText, !searchText.isEmpty else { return }
        model.fetchRepositories(searchText) { [weak self] result in
            switch result {
            case let .success(repositories):
                self?.repositories = repositories

                DispatchQueue.main.async {
                    self?.view?.reloadData()
                }
            case let .failure(error):
                print(error)
            }
        }
    }
}

ご覧にように、表示するリポジトリ一覧であるrepositoriesを保持するのもこの層の役割です。

また、セルの選択や検索ボタンの押下に応じて、該当するリポジトリを返したり、Modelを呼び出してリポジトリ一覧の内容を更新したりするプレゼンテーションロジックを処理します。

MVVM

MVVMは、以下の3つのレイヤーから成るアーキテクチャです。

  • Model:他のアーキテクチャにおけるModelと同じ。ビジネスロジックを担うレイヤー。
  • View:UIイベントを受け付けてViewModelに伝える。ViewModelを監視し、その状態更新を受けて描画処理を行う。
  • ViewModel:Viewからの入力を受け、Modelの処理を呼び出す。Modelからの状態更新を受け取り、自身の状態を更新。Viewに表示するためのデータを保持する。

Viewの説明にある「ViewModelを監視し、その状態更新を受けて描画処理を行う」という処理を可能にするのが、データバインディングと呼ばれる仕組みです。

以下がViewModelの状態更新を受けてViewを更新するために、データバインディングを行なっている部分。

RepositoryViewController.swift
class RepositoryViewController: UIViewController {

    override func viewDidLoad() {

        // 省略

        viewModel.repositories
            .bind(to: tableView.rx.items(cellIdentifier: "RepositoryCell")) { (_, repository, cell: RepositoryCell) in
                cell.configure(repository)
            }
            .disposed(by: disposeBag)

        viewModel.deselectRow
            .bind(to: Binder(self) { viewController, indexPath in
                viewController.tableView.deselectRow(at: indexPath, animated: true)
            })
            .disposed(by: disposeBag)

        viewModel.openURL
            .bind(to: Binder(self) { viewController, url in
                let safariViewController = SFSafariViewController(url: url)
                viewController.present(safariViewController, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
    }

}

ViewControllerには、ViewとViewModelのデータバインディングのみを書くことになるので、だいぶスッキリする印象ですね。

ちなみに、上記の実装でもそうですが、iOSでMVVMというと、ほぼ確実にRxSwift(とRxCocoa)がセットで話題に上がります。
MVVM = RxSwiftというわけでは決してないのですが、MVVMをシンプルに記述するためにも、ほとんどの場合でRxSwiftが導入されるのが現状です。(そして大概RxSwiftの学習コストの高さがデメリットとして指摘されます。)

MVVMは、アーキテクチャ自体に関する理解に加え、RxSwiftの理解とその背後にあるObserverパターンの理解を求められるという点で、広く使われてる割には導入コストが高いアーキテクチャパターンだと感じました。

Flux


(https://facebook.github.io/flux/docs/in-depth-overview/から引用)

  • Action:実行する処理を特定するためのtype(例:"update_repository"のようなラベル)と、それに関連するdata(例:update後の値)がセットになったもの([type : data])。
  • Dispatcher:Actionを受け取り、その値をStoreに伝える。
  • Store:Dispatcherから伝わってきたActionを受けて、自身の状態を更新して通知。
  • View:Storeの状態を監視し、その状態更新を受けて画面を更新。

Fluxアーキテクチャの特徴は「単一方向のデータフロー」という考え方です。

Fluxでは、常にView→Action→Dispatcher→Store→Viewという一方向のデータフローが生成されます。このことのメリットは、アプリケーションが複雑化したり、それに伴って開発者が増えたとしてもデータフローが追いやすく、保守性の高いコード書けることだそうです。

今回作ったアプリは機能的にすごくシンプルなものだったため、このメリットはほとんど享受できていません。そればかりか、慣れない複雑な設計を用いたために、実装しづらかったというのが正直な感想です。

エンジニアであれば、「よりスケールしやすい新しくて重厚なアーキテクチャパターンを使いたい」という気持ちは多かれ少なかれあるでしょうが、アーキテクチャパターンの選定においては、開発するアプリの性質やメンバーの習熟度なども考慮しなければならないと考えると難しいところですね。

実装に関しては、こちらのコードを参考にさせていただきました。

感想

一言で感想を述べると、「やっぱり設計は難しい!!」ということにつきますね。

今回取り上げたものの他に、ReduxClean Architecture, VIPERなども今では主要なアーキテクチャパターンとして知られています。

アーキテクチャパターンを勉強しようとする際の大きな動機の1つとして、「開発するソフトウェアの性質に応じた適切な設計ができるようになりたい」というものがあるでしょう。

私も例外ではなくそう思っていたのですが、主なものだけでもこれだけあるアーキテクチャパターンを十分に理解し、実装できるだけの技術力を身につけ、その上で適切な設計を行えるようにするというのは実際かなり険しい道のりになりそうです…

参考

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

【iOS】Flutterで環境ごとに切り替えビルドする(debug/stg/prod)

プロダクトで開発をしていると、検証等で環境ごとにビルドしたくなる事があると思います。

今回はFlutterのflavorというオプションを使って、

  • Debug → 開発環境(Debugビルド)
  • Staging → ステージング環境(Debugビルド, Releaseビルド)
  • Production → 本番環境(Releaseビルド)

の3つの環境(ビルド構成は4つ)に分けてビルドする方法をまとめていきます。

下準備

環境構築

※Flutterの環境構築がまだだよって方がこちらをどうぞ

FlutterのStableチャンネルを利用してます。
※ 2019/11/28 現在で1.9.1+hotfix.6

チャンネル切り替えは flutter channelでできます。

$ flutter channel stable

flutter --versionを実行してみて以下と同じ感じになればOKです!

$ flutter --version
Flutter 1.9.1+hotfix.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 68587a0916 (2 months ago) • 2019-09-13 19:46:58 -0700
Engine • revision b863200c37
Tools • Dart 2.5.0

※ もし実行時のStableチャンネルではうまく行かないよという場合は flutter versionでversionを指定できるので試してみてください。

プロジェクト生成

flutter createでFlutterプロジェクトを生成します。

$ flutter create \
    --org me.skycat.example.flutter.env \
    --with-driver-test \
    flutter_env_build

※IDEでFlutterProjectを作成してもOKです

プロジェクトが完成したらflutter runしてみて、初期のカウンターアプリが動くか確認しましょう!

iOSで各環境のビルド構成を設定する

Xcode上で各環境のSchemaを作り、xcconfigで各環境ごとにビルド設定をしましょう。

Xcodeプロジェクトを編集する

プロジェクト直下のiosディレクトリ内のRunner.xcodeprojを開いて編集してください。(ios/Runner.xcodeproj)

デフォルトのビルド環境は以下の画像ように、Debug/Release/Profleの3つのビルド構成があり、DebugはDebug.xcconfig、ReleaseとProfileはRelease.xcconfigが設定されています。
image-20191127222943687.png

必要なビルド構成を追加する

今回はDebug/Debug-Staging/Staging/Productionの4つの構成にしたいので、まだないDebug-Staging/Staging/Productionを追加していきましょう!
※ Debug-StagingはStagingのDebugビルド構成です。

[+]を押して、すでにある構成をコピーして作りましょう。
Duplicate "Release" Configurationをクリックして作ります。
作成後はダブルクリックで名前を変更できるので適切な名前(Debug-Staging/Staging/Production)に変更してください。
image-20191127231910372.png
追加が終わったら、不要なRelease/Profleを消しましょう!
以下の画像のようになっていたら完璧です。
image-20191128003802044.png

それぞれの環境用のビルド設定(xcconfig)を追加する

ビルド構成ごとにxcconfigを用意しましょう。
プロジェクト直下のios/Flutterに以下のファイルを追加してください。

Debug.xcconfig
Debug.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Debug
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.debug
DISPLAY_NAME=SAMPLE-debug
Staging.xcconfig
Staging.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Staging
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.stg
DISPLAY_NAME=SAMPLE-stg
Production.xcconfig
Production.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Production
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.prod
DISPLAY_NAME=SAMPLE-prod

追加したものをビルド構成に設定していきます。
image-20191128012152215.png
※ 選択肢に追加したものが出てきていない場合は、Xcodeに認識されないので再追加の必要があります。

ビルド設定(xcconfig)に設定した変数を適用

xcconfigで設定した変数を適用させるためにプロジェクトの設定を変更する必要があります。

アプリの表示名(DISPLAY_NAME)を適用させるために、info.plist`にCFBundleDisplayName$(DISPLAY_NAME)`として定義してください。

    <key>CFBundleDisplayName</key>
    <string>$(DISPLAY_NAME)</string>

image-20191128013615851.png

そしてBundleIDが反映されるように、TARGETS->Runner->IdentityのBundle Identidierを--PRODUCT-BUNDLE-IDENTIFIER-に設定してください。
image-20191128013914316.png

Flavorに対応したSchemaを追加

Flutterのflavorオプションからビルド構成を選択するためにSchemaを3つ追加します。

Product > Schema > Edit Schema
image-20191128001653270.png

Ducplicate Schemaをクリックするとコピーとして作られるので、Schema名をビルド構成と同じ名前に設定します。
image-20191128001825268.png

そしてRuninfoタブを開いて、Build ConfigurationにSchema名と同じビルド構成を選択してください。
StagingとProductionは*Debug executable*のチェックを外してください

↓の画像はDebugの例
image-20191128003602723.png

Debug/Staging/Productionの3つ分が作り終わるとこんな感じになります。

Product > Schema > Schema Manager
image-20191128003449536.png
これでビルドの準備ができました!

Flavorを指定してビルドしてみましょう!
Flavorが未設定(flutter run)だと

表示名が SAMPLE-debug 
BundleIDが me.skycat.example.flutter.env.debug 

FlavorがStaging(flutter run --flavor Staging)だと

表示名が SAMPLE-stg
BundleIDが me.skycat.example.flutter.env.stg 

なアプリがインストールされます!
image-20191128012749343.png
これでiOSのFlavor設定はおわりです!

※ 注意 ※
ProductionはDebugモードの設定いれていないので、Simulationでは実行できません!
(本番環境をSimulationで見ることなんてないやろという考え)

おわりに

「うまくいかない」とか「ここってどうなの?」みたいなことがあったらコメントお願いします🙏
Androidはまた別記事で書きます〜。

そのあとくらいに、
- いろんなCI(Codemagic, Github Actions, Bitrise...etc)での設定
- Flavorでの各種切り替え(Firebaseプロジェクト, ランチャーアイコン, APIの向き先)
などなどの記事も書いていきます〜。

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

【iOS】FlutterでFlavorを使って環境ごとに切り替えてビルドする(debug/stg/prod)

2019/11/28 13:12
AndroidのFlavorと合わせるために一部Flavor名を修正しました(Debug→Develop)[
その他、細かい修正

プロダクトで開発をしていると、検証等で環境ごとにビルドしたくなる事があると思います。

今回は Flutter の flavor というオプションを使って、

  • Develop → 開発環境(Debug ビルド)
  • Staging → ステージング環境(Debug ビルド, Release ビルド)
  • Production → 本番環境(Release ビルド)

の 3 つの環境(ビルド構成は 4 つ)に分けてビルドする方法をまとめていきます。

下準備

環境構築

※Flutter の環境構築がまだだよって方がこちらをどうぞ

Flutter の Stable チャンネルを利用してます。
※ 2019/11/28 現在で 1.9.1+hotfix.6

チャンネル切り替えは flutter channelでできます。

$ flutter channel stable

flutter --versionを実行してみて以下と同じ感じになれば OK です!

$ flutter --version
Flutter 1.9.1+hotfix.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 68587a0916 (2 months ago) • 2019-09-13 19:46:58 -0700
Engine • revision b863200c37
Tools • Dart 2.5.0

※ もし実行時の Stable チャンネルではうまく行かないよという場合は flutter versionで version を指定できるので試してみてください。

プロジェクト生成

flutter createで Flutter プロジェクトを生成します。

$ flutter create \--org me.skycat.example.flutter.env \--with-driver-test \
​    flutter_env_build

※IDE で FlutterProject を作成しても OK です

プロジェクトが完成したら`lutter runしてみて、初期のカウンターアプリが動くか確認しましょう!

iOS で各環境のビルド構成を設定する

Xcode 上で各環境の Schema を作り、xcconfig で各環境ごとにビルド設定をしましょう。

Xcode プロジェクトを編集する

プロジェクト直下の ios ディレクトリ内のRunner.xcodeproを開いて編集してください。(ios/Runner.xcodeproj)

デフォルトのビルド環境は以下の画像ように、Debug/Release/Profle の 3 つのビルド構成があり、
Debug は Debug.xcconfig、Release と Profile は Release.xcconfig が設定されています。
image-20191127222943687.png

必要なビルド構成を追加する

今回は 以下の4つの構成を作ります。

  • Debug-Develop
    • DevelopのDebugビルド構成
  • Debug-Staging
    • StagingのDebugビルド構成
  • Release-Staging
    • StagingのReleaseビルド構成
  • Release-Production
    • ProductionのReleaseビルド構成

[+]を押して、すでにある構成をコピーして作りましょう。
Duplicate "Release" Configurationをクリックして作ります。
作成後はダブルクリックで名前を変更できるので適切な名前に変更してください。

image-20191127231910372.png

追加が終わったら、不要な Release/Profle を消しましょう!
以下の画像のようになっていたら完璧です。
image-20191128124250258.png

それぞれの環境用のビルド設定(xcconfig)を追加する

ビルド構成ごとに xcconfig を用意しましょう。
プロジェクト直下の ios/Flutter に以下のファイルを追加してください。

Develop.xcconfig
Develop.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Develop
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.dev
DISPLAY_NAME=SAMPLE-dev
Staging.xcconfig
Staging.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Staging
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.stg
DISPLAY_NAME=SAMPLE-stg
Production.xcconfig
Production.xcconfig
#include "Generated.xcconfig"
TRACK_WIDGET_CREATION=

FLUTTER_FLAVOR=Production
PRODUCT_BUNDLE_IDENTIFIER=me.skycat.example.flutter.env.prod
DISPLAY_NAME=SAMPLE-prod

追加したものをビルド構成に設定していきます。
image-20191128124620846.png
※ 選択肢に追加したものが出てきていない場合は、Xcode に認識されないので再追加の必要があります。

ビルド設定(xcconfig)に設定した変数を適用

xcconfig で設定した変数を適用させるためにプロジェクトの設定を変更する必要があります。

アプリの表示名(DISPLAY_NAME)を適用させるために、
info.plistCFBundleDisplayName$(DISPLAY_NAME)として定義してください。

​    <key>CFBundleDisplayName</key>
​    <string>$(DISPLAY_NAME)</string>

image-20191128013615851.png

そして BundleID が反映されるように、
TARGETS->Runner->BuildSettings の Product Bundle Identifier を削除(フォーカスした状態でDeleteKeyでOK)して、
info.plistのCFBundleIdentifier(.xcconfigで設定しているPRODUCT_BUNDLE_IDENTIFIER)を読み込むようにします。
image-20191128124620846.png

Flavor に対応した Schema を追加

Flutter の flavor オプションからビルド構成を選択するために Schema を 3 つ追加します。

Product > Schema > Edit Schema
image-20191128001653270.png

Ducplicate Schemaをクリックするとコピーとして作られるので、Schema 名をビルド構成と同じ名前に設定します。
image-20191128001825268.png

そしてRunのinfoタブを開いて、Build Configurationに Schema 名と同じビルド構成を選択してください。

StagingとProductionは*Debug executable*のチェックを外してください
(Debug表示がなくなってログが吐かれなくなります、必要な人は外さなくてもいいです)

↓ の画像は Develop の例
image-20191128130850245.png

ReleaseビルドのあるStagingとProductionは ProfileArchivesBuild Configuration設定してください。
StagingはRelease-StagingProductionはRelease-Productionになります、

Develop/Staging/Production の 3 つ分が作り終わるとこんな感じになります。

Product > Schema > Schema Manager
image-20191128003449536.png

これでビルドの準備ができました!
Flavor を指定してビルドしてみましょう!

Flavor が Staging(flutter run --flavor Develop)だと

表示名が SAMPLE-dev
BundleIDが me.skycat.example.flutter.env.dev

Flavor が Staging(flutter run --flavor Staging)だと

表示名が SAMPLE-stg
BundleIDが me.skycat.example.flutter.env.stg

なアプリがインストールされます!
image.png
これで iOS の Flavor 設定はおわりです!

※ 注意 ※
Production は Debug モードの設定いれていないので、Simulation では実行できません!
(本番環境を Simulation で見ることなんてないやろという考え)

おわりに

「うまくいかない」とか「ここってどうなの?」みたいなことがあったらコメントお願いします 🙏
Android はまた別記事で書きます〜。

そのあとくらいに、

  • いろんな CI(Codemagic, Github Actions, Bitrise...etc)での設定

  • Flavor での各種切り替え(Firebase プロジェクト, ランチャーアイコン, API の向き先)

などなどの記事も書いていきます〜。よろしくおねがいします🙏

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

Objective-C Bridging Headerファイルを編集したら大量のビルドエラーが出る時の対処

なにが起きたか

Objective-C Bridging Headerファイル(Objective-C-Bridging-Header.hとか)を編集した後にビルドしたら大量のビルドエラーが発生した:innocent:
Objective-C_Bridge_Header_Change.png

対処

  • エラーが出てるファイルにimport UIKit を追記する
    無理でしょ…

  • Objective-C Bridging Headerファイルに#import <UIKit/UIKit.h>を追記する
    NSObjectなどのエラーが出ている場合は #import <Foundation/Foundation.h>を追記する

参考

https://stackoverflow.com/questions/26116288/failed-to-import-bridging-header
UIKitとかFoundationがなくても、ライブラリが機能するようにということらしい。

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