20191126のiOSに関する記事は13件です。

Swiftでビジネスロジックを実行するUseCaseのprotocolを作りたい話 2019

最初にこの文章の結論

この文章は、ロジックを処理する次のような型をアプリケーションの中で定義してみたらどうかな、と考えているのを文章にしてみたものです。

protocol UseCase {
    associatedtype Parameters
    associatedtype Success

    func execute(
      _ parameters: Parameters, 
      completion: ((Result<Success, Error>) -> ())?
    )

    func cancel()
}

なぜこんな事を考えているかというと、iOS VIPERアーキテクチャ研究読本(仮)という電子書籍を作ってみたいなと考えていて、まずはサンプルコードを作ろうとしているためです。

VIPER研究読本用サンプルコードのリポジトリ
https://github.com/yimajo/VIPERBook1Samples

UseCaseとは?

この記事でのUseCaseの前提として、

  • システムを水平レイヤに分割したときのビジネスロジックを実装するもの
  • 単一の目的を持ったコンポーネント

としてます。

この前提は、書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」での"ユースケース"から参考にしました。

注文入力システムに注文を追加するユースケースは、注文を削除するユースケースと比べると、明らかに異なる頻度と理由で変更される。ユースケースはシステムを分割する自然な方法である。また、ユースケースは、システムの水平レイヤーを薄く垂直にスライスしたものである。それぞれのユースケースは、UIの一部、アプリケーション特有のビジネスルールの一部、アプリケーションに依存しないビジネスルールの一部、データベース機能の一部を使用する。したがって、システムを水平レイヤーに分割するときには、それらを薄く垂直にユースケースとしても分割するのである。

ここまでで述べているのは、まず2つのユースケースがあるということ、そしてその分割方向についてです。

そして同書籍の中でユースケースの単位について「注文追加」「注文削除」を少し掘り下げています。それを読むと注文追加がaddOrderで注文削除がdeleteOrderとのこと

ユースケースがお互いに切り離されていれば、addOrder(注文追加)のユースケースにフォーカスしたチームがdeleteOrder(注文削除)のユースケースにフォーカスしたチームの邪魔をする可能性は低い。

一応書いておくんですが、ユースケースは2つあるとは書いてあるもののそれらは2つのclassやstructとは書いてないんですね。むしろaddOrderdeleteOrderというのがメソッドにすら見える。そうなると「1つのclass/structに2つのメソッドがある」のかそれとも「2つのclass/structにそれぞれ1つのメソッドがある」のかということが気になってくるのですが、今回の話では後者の「2つのclass/structにそれぞれ1つのメソッドがある」ということでやっていきます。

具体例

同書籍に書いてある内容をざっくりコードに落とすと、だいたい次のようなことだと思います

struct AddOrderUseCase {
  func execute(_ order: Order)
}

struct DeleteOrder {
  func execute(_ orderID: OrderID)
}

ここからテスト自動化できるようにしていくことや、その他のことを雑に考えるとprotocolを作ったりしたくなるというわけですよね。

// AddOrder
protocol AddOrderUseCase {
  func execute(_ order: Order)
}

struct AddOrderDefaultUseCase: AddOrderUseCase {
  func execute(_ order: Order)
}

struct テスト用AddOrderUseCase: AddOrderUseCase {
  func execute(_ order: Order)
}

// DeleteOrder

protocol DeleteOrderUseCase {
  func execute(_ orderID: OrderID)
}

struct DeleteOrderDefaultUseCase: DeleteOrderUseCase {
  func execute(_ orderID: OrderID)
}

struct テスト用OrderDefaultUseCase: DeleteOrderUseCase {
  func execute(_ orderID: OrderID)
}

で、こういうことをやっていくともっと良いアプローチが無いかなと思うじゃないですか。...というのが発端です。

他の言語/プラットフォームではどうやってる?

Ruby on Railsでの例

まずは全然違う例としてサーバサイドを取り上げます。Ruby on RailsでオレオレUseCaseを考えている人たちも勿論いるので興味深いわけです。

A Case For UseCase
https://webuild.envato.com/blog/a-case-for-use-cases/

上の記事でやりたいことを勝手に要約すると

  • 写真を買うというユースケースがある
    • class PurchasePhotoの内容
    • 購入時の細かい動作としては次の動作がある
      • Purchaseテーブルにinsert
      • 請求書の送付
      • カウンタのインクリメント
      • レビューの権利を付与する
  • class PurchasePhotoの作り方として
    • 抽象化したUseCaseというmoduleを作る
    • PurchasePhotoはmoduleをimportする

コードとしては次のようにstaticにアクセスして上記3つの動作をさせたいわけです。

PurchasePhoto.perform(buyer, photo) 

写真購入のほかのパターンは書かれてませんが、
必要な情報はパラメータとしてメソッド実行時に揃ってますし、良さそうに見える。

ただもともとデータベースに結果を入れていることもあり、テストコードはそのデータベースの値を見れば分かるようになっています。

しかし我々iOSアプリ開発するときはそういうことばっかりじゃないわけです。大抵は通信してJSONに色を付けないといけなかったりしますんで、その通信の結果を見るという作りからさらに改良してDBに保存してテストできるようにするというのは手間に感じます。

さらに非同期処理に対応しないといけない。というわけでこっからモバイルアプリのAndroidでのやり方を参考にしていきます。

Android Blue Print - todo-mvp-clean の場合

Android Blue Printを見てみます。

TODOアプリを様々な作り方でブランチごとに分けていて、MVPでClean Architectureっぽく作られてまして、そこでUseCaseはabstract classになってるわけです。

public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue>

abstract class UseCase

もう少し細かい特徴は

  • RequestとResponseの型を渡す
  • 外からはexecuteUseCase メソッドで呼び出される
    • mUseCaseCallbackに成功か失敗を送る

GetTaskのUseCaseを例にとった利用シーンとしては次のような感じ

mUseCaseHandler.execute(mGetTask, new GetTask.RequestValues(mTaskId),
                new UseCase.UseCaseCallback<GetTask.ResponseValue>() {
                    @Override
                    public void onSuccess(GetTask.ResponseValue response) {
                        showTask(response.getTask());
                    }

                    @Override
                    public void onError() {
                        showEmptyTaskError();
                    }
                });

外部からはコールバックのハンドラーを登録して結果はそれが動作するイメージ。

同じパターンであることを示すためにSaveTaskも抜粋すると次のように共通点から、差分を見て具体的に何がやりたいかが分かるはずです。

mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask),
        new UseCase.UseCaseCallback<SaveTask.ResponseValue>() {
            @Override
            public void onSuccess(SaveTask.ResponseValue response) {
                mAddTaskView.showTasksList();
            }

            @Override
            public void onError() {
                showSaveError();
            }
        });

google/iosched の場合

同じAndroidで参考にしたいのは、Google IOアプリのコードgoogle/ioschedです。READMEでは「データレイヤーとプレゼンテーションレイヤーの間に、lightweight domain layer実装した。UIスレッドとは別にビジネスロジック処理する」と書いてUseCaseもあります。

UseCase
https://github.com/google/iosched/blob/master/shared/src/main/java/com/google/samples/apps/iosched/shared/domain/UseCase.kt

invokeメソッドは大きく分けて2種類

  • ObserverパターンのLiveDataを返す
    • operator fun invoke(parameters: P): LiveData<Result<R>>
  • 即時実行してResultを返す
    • fun executeNow(parameters: P): Result<R>

これは前述のAndroid Blue Print - todo-mvp-clean で結果を全てコールバックで取得していたのと比較すると、「コールバックして返す必要のないものを戻り値としてすぐ取得できる」というメリットを感じます。

さて、実際の利用例として同期的に取得しているSearchUseCaseは次のような感じです。

まずは簡単なexecuteNowから

val result = useCase.executeNow(parameters = "session 0")

つぎにObserverパターンのLiveDataを返していたほうoperator fun invokeしているとおそらくobserveメソッドで呼び出すっていう感じになるんだと思います。

val resultLiveData = useCase.observe()
useCase.execute(FeedbackParameter(
    "userIdTest",
    TestData.userEvents[0],
    TestData.userEvents[0].id,
    mapOf("q1" to 1)
))
// 結果を取り出して
val result = LiveDataTestUtil.getValue(resultLiveData)
// テストしてる
assertEquals(Result.Success(Unit), result)

でまあ何が言いたいかというと、結果をコールバックで返す処理/結果を戻り値で返す処理それぞれに応じたインタフェースをabstract classとして用意していて、どちらかを実装していればそれが使えるし、実装していない方は使えないようになってるんじゃないでしょうか。

本題: Swift でUseCaseをつくる

これをGitHubのWeb APIを利用したリポジトリ検索のビジネスロジックとしてUseCaseを作ってみようってのが本題です。

一番最初に述べたUseCaseをもう一度書いておきます。

protocol UseCase {
    associatedtype Parameters
    associatedtype Success

    func execute(
      _ parameters: Parameters,
      completion: ((Result<Success, Error>) -> ())?
    )

    func cancel()
}

今回は1つのexecuteメソッドとcancelメソッドのみとし、executeメソッドはクロージャによって結果を取得します。できれば先述のexecuteNowのように単純なインタフェースを増やしたいところですが、OptionalなprotocolメソッドをSwiftで表現できないのが残念なところです。

なお、これまで紹介したUseCaseではcancelすることについて触れていませんでした。話がややこしくなるのでそこを掘り下げたりしないほうが良いかと思っています。RxSwiftなどを使ってObservableを戻り値に返せばそれを使って自動的にキャンセルもできるし、そもそもメソッドもコールバックなしのシンプルなものが用意できますが、一旦それは忘れましょう。

続いてこのUseCaseに準拠した具体的なclassについて考えます。GithubRepoSearchInteractorではWeb APIにアクセスするGithubRepoSearchAPIRequestを利用してその結果をクロージャで取得できるようにしています。

class GithubRepoSearchInteractor: UseCase {

    var request: GithubRepoSearchAPIRequest?

    func execute(_ parameters: String,
                 completion: ((Result<[GithubRepoEntity], Error>) -> ())?) {
        let request = GithubRepoSearchAPIRequest(word: parameters)
        request.perform { result in
            switch result {
            case .success(let response):
                completion?(.success(response.items))
            case .failure(let error):
                completion?(.failure(error))
            }
        }

        self.request = request
    }

    func cancel() {
        request?.cancel()
    }
}

実際に利用するのは次のようになると思えるでしょう。

let githubRepoSearch: UseCase = GithubRepoSearchInteractor()

引数に使うんだったら

func なにかの関数(githubRepoSearch: UseCase) {
   // ... 省略
}

しかしこれはコンパイルできません。

何が問題か

UseCaseのprotocolがassociatedtypeを使っていて、その型が解決していないことで次のようにエラーメッセージが表示されます。

Protocol 'UseCase' can only be used as a generic constraint because it has Self or associated type requirements

これをなんとかしなければいけない。

Type Erasure: 継承 box 方式を使う

というわけでこのprotocolをそのまま使うのではなく、
AnyUseCaseclassの引数にしつつ抽象的な扱いをするようにしたいわけです。

let githubRepoSearch: AnyUseCase<String, [GithubRepoEntity]> = AnyUseCase(GithubRepoSearchInteractor())

参考にさせてもらったのは次の記事の「type erasure: 継承 box 方式」
https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0

実際にやってみると

// 実際の型情報として利用されるAnyUseCse
final class AnyUseCase<Parameters, Success>: UseCase {
    // UseCaseの実体。Parameters, Successの型を合わせること
    private let box: AnyUseCaseBox<Parameters, Success>

    init<T: UseCase>(_ base: T) where T.Parameters == Parameters, T.Success == Success {
        box = UseCaseBox<T>(base)
    }

    func execute(_ parameters: Parameters, completion: ((Result<Success, Error>) -> ())?) {
        box.execute(parameters, completion: completion)
    }
    func cancel() {
        box.cancel()
    }
}

// MARK: - AnyUseCaseさえ知ってればいい情報

private extension AnyUseCase {
    class AnyUseCaseBox<Parameters, Success> {
        func execute(_ parameters: Parameters, completion: ((Result<Success, Error>) -> ())?) {
            fatalError()
        }

        func cancel() {
            fatalError()
        }
    }

    // Parameters, Success を UseCase のそれと合わせるために AnyUseCaseBox を継承する
    final class UseCaseBox<T: UseCase>: AnyUseCaseBox<T.Parameters, T.Success> {
        private let base: T

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

        override func execute(_ parameters: T.Parameters, completion: ((Result<T.Success, Error>) -> ())?) {
            base.execute(parameters, completion: completion)
        }

        override func cancel() {
            base.cancel()
        }
    }
}

登場人物それぞれは次の役割となっています

  • AnyUseCse
    • 実際の型情報として外から利用する
    • インタフェースを内部の AnyUseCaseBox に保持して利用する
  • AnyUseCaseBox
    • Parameters, Success を UseCase のそれと合わせるため
  • UseCaseBox
    • 内部に UseCase 自体を保持したい

具体的にこのAnyUseCseを利用するときは次のようになるはずです

func sample(_ githubRepoSearch: AnyUseCase<String, [GithubRepoEntity]>) {
    githubRepoSearch.execute("検索したいワード") { result in
        switch result {
        case .success(let items):
          // ... 省略
        case .failure(let error):
          // ... 省略
        }
    }
}

sample(AnyUseCase(GithubRepoSearchInteractor()))

デメリット

全てコールバックでデータを取得することになる

同期的に戻り値で取得するようなデータでさえ、クロージャを使いコールバックされることになります。

最後に

もうちょっと他の良いやり方はないもんでしょうかね?他の案を比較できればもっと良くできるのではと思ってます...

みなさんはどんなUseCaseを作ってますか?

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

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

はじめに

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

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

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

今回は、位置情報を取得します。

位置情報の取得

  1. CoreLocationをインポート

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

    • エディタで、以下のように修正
    • 【なぜ?】
      • この変数を通してプログラムで位置情報を取り扱うため
    ViewController.swift
    class ViewController: UIViewController {
        var locationManager: CLLocationManager!  // この行を追加
    
        override func viewDidLoad() {
    
  3. CoreLocation の delegate の使用を宣言(プロトコルの継承)

    • エディタで、以下のように修正
    • 【なぜ?】
      • プロトコルを継承することで、位置情報更新などのイベントに対する処理をプログラムすることができるようになる
      • 今回は ViewController.swift で継承したことで、クラスが増えずシンプルな実装とすることができた
      • (今回はしないが)別クラスで継承・実装すれば再利用性の高いクラスとできると思う
    ViewController.swift
    class ViewController: UIViewController, CLLocationManagerDelegate {  //この行を修正
        var locationManager: CLLocationManager!
    
        override func viewDidLoad() {
    
  4. CoreLocation 変数の初期化とCoreLocationの初期処理

    • エディタで、以下のように修正
    • 【なぜ?】
      • 変数は、初期化しないと使えない
      • 位置情報更新時のイベントを処理するプログラムを指定する必要がある
      • 明示的に位置情報取得開始を指示する必要がある
      • 利用者から、このアプリで位置情報を使う許可をもらう必要がある
    ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    
        // この行↓から
        locationManager = CLLocationManager()  // 変数を初期化
        locationManager.delegate = self  // delegateとしてself(自インスタンス)を設定
    
        locationManager.startUpdatingLocation()  // 位置情報更新を指示
        locationManager.requestWhenInUseAuthorization()  // 位置情報取得の許可を得る
        // この行↑までを追加
    }
    
  5. 位置情報更新時に呼び出される処理を記述

    • エディタで、以下のように修正
    • 【なぜ?】
      • 位置情報更新時に呼び出される処理
      • 最新(last)の位置情報から緯度経度を取り出している
      • 地図と連携する場合は、最新の位置を用いて地図を更新すればよいはず
    ViewController.swift
    override func viewDidLoad() {
        // 表示の都合上、プログラムを省略しています
    }
    
    // この行↓から
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
        let longitude = (locations.last?.coordinate.longitude.description)!
        let latitude = (locations.last?.coordinate.latitude.description)!
        print("[DBG]longitude : " + longitude)
        print("[DBG]latitude : " + latitude)
    }
    // この行↑までを追加
    
  6. 位置情報使用許可文字列を設定

    • 画面左側のファイルツリーから[MyGpsMap]-[Info.plist]を選択
    • Ctrl+クリックでコンテキストメニューを表示し[Open As]-[Source Code]を選択(↓のようにテキストエディタ形式で表示される)
      • 【なぜ?】
        • セキュリティ上の理由により、利用許可を得る必要がある
          InfoList-edit.png
    • ファイルの後ろ付近に以下の行を追加
      • 【なぜ?】
        • 利用許可画面に表示される文字列
        • この文字列設定が存在しないと、利用許可を完了できず位置情報が取得できない
    Info.plist
        </array>
        <!-- ここ↓から追加 -->
        <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
        <string>常に位置情報の利用を許可する理由/目的を書く</string>
        <key>NSLocationWhenInUseUsageDescription</key>
        <string>アプリ起動時に位置情報の利用を許可する理由/目的を書く</string>
        <!-- ここ↑まで追加 -->
    </dict>
    </plist>
    
  7. テスト実行

    • Xcode 左上の矢印アイコンをクリック
    • 位置情報利用確認画面が表示される
      AllowLocationAccess.png
    • Simulator のメニューから[Debug]-[Location]-[City Run]を選択することでGPSが更新される
      UpdateLocation.png

今回の到達点

  • 位置情報(GPS)が取得できるようになった

参考情報

ViewController.swift
//
//  ViewController.swift
//  MyGpsMap
//
//  Created by shinobee on 2019/11/24.
//  Copyright © 2019 shinobee. All rights reserved.
//

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
    var locationManager: CLLocationManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        locationManager = CLLocationManager()
        locationManager.delegate = self

        locationManager.startUpdatingLocation()
        locationManager.requestWhenInUseAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
        let longitude = (locations.last?.coordinate.longitude.description)!
        let latitude = (locations.last?.coordinate.latitude.description)!
        print("[DBG]longitude : " + longitude)
        print("[DBG]latitude : " + latitude)
    }
}

連載

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

MobileNetV2 サンプルコードを解読

概要

数回に渡って、CoreModelの学習モデルのiOSでの使い方について、サンプルコードのCoreMLに関わる部分を中心に解読していく。

・参考URL
https://developer.apple.com/jp/machine-learning/models/#text

今回はMobileNetV2を解読する。

MobileNetV2

MLModelセットアップ

// MARK: - Image Classification

    /// - Tag: MLModelSetup
    lazy var classificationRequest: VNCoreMLRequest = {
        do {
            /*
             Use the Swift class `MobileNet` Core ML generates from the model.
             To use a different Core ML classifier model, add it to the project
             and replace `MobileNet` with that model's generated Swift class.
             */
            let model = try VNCoreMLModel(for: MobileNet().model)

            let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
                self?.processClassifications(for: request, error: error)
            })
            request.imageCropAndScaleOption = .centerCrop
            return request
        } catch {
            fatalError("Failed to load Vision ML model: \(error)")
        }
    }()

一箇所ずつ何をやっているか解読。

// MobileNet modelからMobileNetクラスを生成し、そのクラスからインスタンスを生成
let model = try VNCoreMLModel(for: MobileNet().model)
// VNCoreMLRequestオブジェクトを生成、
// リクエスト後、モデルから結果を受け取るメソッドを指定するためにcompletionHandlerを使用。 
let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
                self?.processClassifications(for: request, error: error)
            })
// 画像のスケール方法を決定
request.imageCropAndScaleOption = .centerCrop

リクエスト実行

/// - Tag: PerformRequests
    func updateClassifications(for image: UIImage) {
        classificationLabel.text = "Classifying..."

        let orientation = CGImagePropertyOrientation(image.imageOrientation)
        guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }

        DispatchQueue.global(qos: .userInitiated).async {
            let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
            do {
                try handler.perform([self.classificationRequest])
            } catch {
                /*
                 This handler catches general image processing errors. The `classificationRequest`'s
                 completion handler `processClassifications(_:error:)` catches errors specific
                 to processing that request.
                 */
                print("Failed to perform classification.\n\(error.localizedDescription)")
            }
        }
    }
// 画像の向きを取得
// imageからCIImageに変換
let orientation = CGImagePropertyOrientation(image.imageOrientation)
        guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }
DispatchQueue.global(qos: .userInitiated).async {
            let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
            do {
                try handler.perform([self.classificationRequest])
            } catch {
                /*
                 This handler catches general image processing errors. The `classificationRequest`'s
                 completion handler `processClassifications(_:error:)` catches errors specific
                 to processing that request.
                 */
                print("Failed to perform classification.\n\(error.localizedDescription)")
            }
        }

プロセス分類

/// Updates the UI with the results of the classification.
    /// - Tag: ProcessClassifications
    func processClassifications(for request: VNRequest, error: Error?) {
        DispatchQueue.main.async {
            guard let results = request.results else {
                self.classificationLabel.text = "Unable to classify image.\n\(error!.localizedDescription)"
                return
            }
            // The `results` will always be `VNClassificationObservation`s, as specified by the Core ML model in this project.
            let classifications = results as! [VNClassificationObservation]

            if classifications.isEmpty {
                self.classificationLabel.text = "Nothing recognized."
            } else {
                // Display top classifications ranked by confidence in the UI.
                let topClassifications = classifications.prefix(2)
                let descriptions = topClassifications.map { classification in
                    // Formats the classification for display; e.g. "(0.37) cliff, drop, drop-off".
                   return String(format: "  (%.2f) %@", classification.confidence, classification.identifier)
                }
                self.classificationLabel.text = "Classification:\n" + descriptions.joined(separator: "\n")
            }
        }
    }

上記は、分類結果をラベルに表示している。

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

実機iPhoneのSafariでWebインスペクタを使ってデバッグする方法

要望

実機のiPhoneで特定のWebページに対し、Webインスペクタで中身を書き換えたり、コンソール経由でスクリプトを叩いて実行した結果を見たい。

-> Android版

iPhoneのSafariにもWebインスペクタがある

iPhone単体ではView Sourceなどのアプリを使うことで、Webページのソース確認はできるものの、Webインスペクタやコンソールによるスクリプト実行が出来ない。
スクリーンショット 2019-11-26 16.23.27.png

また、MacのSafariやiOSエミュレーターでもある程度の表示確認はできるが、やはり実機での操作感や見た目の確認は重要である。

スクリーンショット 2019-11-26 16.25.39.png

少し調べてみたところ、MacとiPhoneを接続することで、PCと同様に実機での動作確認が可能なことが判明したため、実際に試してみた。

iPhone Safariの設定

事前準備としてiPhone側の設定を行う。「設定」を開き「Safari」をタップ。
IMG_1943.PNG

画面下の「詳細」をタップ。
IMG_1944.PNG

「Webインスペクタ」をONにする。

IMG_1945.PNG
iPhone側の設定はこれで完了。

Mac Safariの設定

Safariの「環境設定」を開き、「詳細」タブの「メニューバーに"開発"メニューを表示」にチェックを入れる。
スクリーンショット 2019-11-26 16.19.11.png

MacとiPhoneをケーブル接続。Safariの「開発」から実機iPhoneを選択し、対象サイトを選択。
スクリーンショット 2019-11-26 16.03.28.png

Mac上のWebインスペクタが開く。
スクリーンショット 2019-11-26 16.04.56.png

通常と同じ操作感で要素検証や書き換えが可能。
IMG_1950.PNG
もちろんコンソールも使えるのでスクリプトを叩いて実行することも可能。
スクリーンショット 2019-11-26 16.05.54.png
いえーい
IMG_1951.PNG
Macに接続したiPhoneが認識されないことがあるけど、ケーブル抜き差ししてると出てくる。

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

AppStoreやAppleDeveloperProgramまわりの忘れがちなこと

対象

iOS開発に慣れて来た頃に、証明書の更新などたまにしかやらない故に忘れがちなことをまとめていきます。
一度は、証明書は完全に理解した!と思った方向けです。

仕様が頻繁に変わるので各項目ごとに最終確認日を掲載しています。

Apple Developer Programの場合です。Apple Enterprise Developer Programでは違う箇所があるのでご注意下さい。

iOS Distributionの期限が切れたときの心配事

確認日2019/05/07

  • Adhoc/Productionでのビルドができなくなる
  • ストア公開中のアプリには影響ない
  • Adhoc配信したアプリは起動できなくなる
  • Push通知は届く

証明書って何個まで作れる?

確認日2019/05/07

上限まで作成すると以下の文言が表示されます
(Maximum number of certificates generated)

  • Entity Typeが「Individual」 (個人または個人事業主/個人経営者)の場合

    • iOS Distribution:2
    • iOS Development:3
  • Entity Typeが「Company / Organization」(組織) の場合

    • iOS Distribution:3
    • iOS Development:6
  • Entity Typeが「In-House / Enterprise」の場合

    • iOS Distribution:未確認
    • iOS Development:未確認だが100くらいいけそう (情報求む)

Apple Developer Programの有効期限は?

確認日2019/06/04

  • iOS Developer University Program
    • 有効期限なし

証明書の有効期限は?

確認日2019/06/04

  • Entity Typeが「Individual」 (個人または個人事業主/個人経営者)の場合

    • iOS Distribution:2
    • iOS Development:3
  • Entity Typeが「Company / Organization」(組織) の場合

    • iOS Distribution:3
    • iOS Development:6
  • Entity Typeが「In-House / Enterprise」の場合

    • iOS Distribution:未確認
    • iOS Development:未確認だが100くらいいけそう (情報求む)

Push通知が届かない期間は?

  • Push通知証明書を別途作成してAppIDに設定 〜 外部サービスに登録、までは届かなくなる?

    • Development / Production共に2つずつPush通知証明書を設定可能なのでスキマはない
  • AppIDsにはPush証明書Production/Development2つずつ割り当て可能

    • とはいえ、Push通知の外部サービスに証明書を登録するので同時に使える証明書は1つ

Provisioning Profile

Provisioning Profileはアプリ内包なので、インストール済アプリには変更が効かない
ただ、基本、Distribution/Development証明書より1ヶ月期限が長いはず

おわり

変更や間違っている箇所があればコメントお願いします!
年1の作業とか毎年忘れますね。

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

UITableViewController の タブボタンをカスタマイズしてみる

0.はじめに

UITableViewController のタブボタンを色々とカスタマイズしたかったので、やってみました。

試してみたのは、以下。

  • タブボタンのサイズを個別に変更する
  • タブボタンの背景色を個別に変更する
  • どのタブも初期選択されない様にしてみる

ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。

IMG_3332.PNG

本当は、4つタブがあるんですが、表示されていません。

1.コード

TabBarController.swift
//
//  TabBarController.swift
//  UITableViewController-Sample01
//
//  Created by Kusokamayarou on 2019/11/26.
//  Copyright © 2019 Makurazakiutoya. All rights reserved.
//

import UIKit

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        // 「AutoLayoutをコードから指定する, アニメーションさせながらAutoLayoutを変更する - Qiita」
        // <https://qiita.com/yokurin/items/4932ab488b5b503f2dd5>
        // 「ios - Swift | Adding constraints programmatically - Stack Overflow」
        // <https://stackoverflow.com/questions/26180822/swift-adding-constraints-programmatically>
        let tabBarButtons = self.tabBarButtons()
        // item 1
        tabBarButtons![0].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .top, relatedBy: .equal,
                toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .bottom, relatedBy: .equal,
                toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .left, relatedBy: .equal,
                toItem: tabBar, attribute: .left, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .width, relatedBy: .equal,
                toItem: nil, attribute: .notAnAttribute,  multiplier: 1, constant: 0)
            ])
        // item 2
        tabBarButtons![1].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .top, relatedBy: .equal,
                toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .bottom, relatedBy: .equal,
                toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .left, relatedBy: .equal,
                toItem: tabBarButtons![0], attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .width,  relatedBy: .equal,
                toItem: tabBar, attribute: .width,  multiplier: 0.3333, constant: 0)
            ])
        // item 3
        tabBarButtons![2].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .top, relatedBy: .equal,
                               toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .bottom, relatedBy: .equal,
                               toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .left, relatedBy: .equal,
                               toItem: tabBarButtons![1], attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .right, relatedBy: .equal,
                               toItem: tabBarButtons![3], attribute: .left, multiplier: 1, constant: 0)
            ])
        // item 4
        tabBarButtons![3].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .top, relatedBy: .equal,
                               toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .bottom, relatedBy: .equal,
                               toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .right, relatedBy: .equal,
                               toItem: tabBar, attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .width,  relatedBy: .equal,
                               toItem: tabBar, attribute: .width,  multiplier: 0.3333, constant: 0)
            ])
        // ios - How to definitively set UITabBar background color and UITabBar tint color - Stack Overflow
        // https://stackoverflow.com/questions/37626377/how-to-definitively-set-uitabbar-background-color-and-uitabbar-tint-color
        self.tabBar.barTintColor = .systemBackground
    }
    // 「小ネタ:UITabBarControllerに「モーダル表示するボタン」を追加する(Swift) - Qiita」
    // <https://qiita.com/paming/items/a1413480358fa81728cf>
    override func viewDidLayoutSubviews() {
        let tabBarButtons = self.tabBarButtons()
        UIView.setAnimationsEnabled(false)
        // item 1
        tabBarButtons![0].alpha = 0.0
        tabBarButtons![0].backgroundColor = .red
        tabBarButtons![0].layoutIfNeeded()
        // item 2
        tabBarButtons![1].backgroundColor = .cyan
        tabBarButtons![1].layoutIfNeeded()
        // item 3
        tabBarButtons![2].backgroundColor = .green
        tabBarButtons![2].layoutIfNeeded()
        // item 4
        tabBarButtons![3].backgroundColor = .yellow
        tabBarButtons![3].layoutIfNeeded()
        UIView.setAnimationsEnabled(true)
    }
}

extension TabBarController: UITabBarControllerDelegate {
    // 「swiftでUITabBarの特定のタブをタップした時にモーダル - Qiita」
    // https://qiita.com/higan96/items/5ea742b59a48a34baa32
    internal func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        self.viewDidLayoutSubviews()
        return true
    }
    // 「[Swift]タブ切り替え時に切り替え先のメソッドを実行する」
    // https://nobuhiroharada.net/2018/04/06/change-tab/
    internal func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
    }
}

// 「Swift - UITabbarControllerにモーダル表示するボタンを追加(68046)|teratail」
// <https://teratail.com/questions/68046>
extension TabBarController {
    func tabBarButtons() -> [UIView]? {
        return self.tabBar.subviews.reduce([], {
            (ret: [UIView], item:AnyObject) -> [UIView] in if let v = item as? UIView {
                if v.isKind(of: NSClassFromString("UITabBarButton")!) {
                    return ret + [v]
                }
            }
            return ret
        })
    }
}
■ タブボタンにアクセスするには?

これが無いと何も変更できません…。

  1. 以下の記事を参考に、tabBarButtons() を作成します。
■ タブボタンのサイズを個別に変更するには?
  1. 以下の記事を参考に、viewDidLoad() で各タブボタンに Constraints を設定します。
■ タブボタンの背景色を個別に変更するには?
  1. 以下の記事を参考に、viewDidLoad() でタブバーの背景色を設定します。
  2. 以下の記事を参考に、viewDidLayoutSubviews() を作成し、初期表示時のタブボタンの背景色を設定します。
  3. 以下の記事を参考に、tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) を作成し、タブ選択時のタブボタンの背景色を設定します。
■ どのタブも初期選択されない様するには?

これまでの設定の応用になります。

  1. 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
  2. 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。

99.ハマりポイント

  • 結構、ハマったと思いますが…、結構前にやったので覚えてません…。

???

XX.まとめ

他にも色々とカスタマイズ出来るかもしれませんね♪

以下、GitHub にも UP してますので、参考になれば♪

?‍♂️?‍♂️?‍♂️

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

UITabBarController の タブボタンをカスタマイズしてみる

0.はじめに

UITabBarController のタブボタンを色々とカスタマイズしたかったので、やってみました。

試してみたのは、以下。

  • タブボタンのサイズを個別に変更する
  • タブボタンの背景色を個別に変更する
  • どのタブも初期選択されない様にしてみる

ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。

IMG_3332.PNG

本当は、4つタブがあるんですが、表示されていません。

1.コード

TabBarController.swift
//
//  TabBarController.swift
//  UITableViewController-Sample01
//
//  Created by Kusokamayarou on 2019/11/26.
//  Copyright © 2019 Makurazakiutoya. All rights reserved.
//

import UIKit

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        // 「AutoLayoutをコードから指定する, アニメーションさせながらAutoLayoutを変更する - Qiita」
        // <https://qiita.com/yokurin/items/4932ab488b5b503f2dd5>
        // 「ios - Swift | Adding constraints programmatically - Stack Overflow」
        // <https://stackoverflow.com/questions/26180822/swift-adding-constraints-programmatically>
        let tabBarButtons = self.tabBarButtons()
        // item 1
        tabBarButtons![0].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .top, relatedBy: .equal,
                toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .bottom, relatedBy: .equal,
                toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .left, relatedBy: .equal,
                toItem: tabBar, attribute: .left, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![0], attribute: .width, relatedBy: .equal,
                toItem: nil, attribute: .notAnAttribute,  multiplier: 1, constant: 0)
            ])
        // item 2
        tabBarButtons![1].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .top, relatedBy: .equal,
                toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .bottom, relatedBy: .equal,
                toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .left, relatedBy: .equal,
                toItem: tabBarButtons![0], attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![1], attribute: .width,  relatedBy: .equal,
                toItem: tabBar, attribute: .width,  multiplier: 0.3333, constant: 0)
            ])
        // item 3
        tabBarButtons![2].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .top, relatedBy: .equal,
                               toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .bottom, relatedBy: .equal,
                               toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .left, relatedBy: .equal,
                               toItem: tabBarButtons![1], attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![2], attribute: .right, relatedBy: .equal,
                               toItem: tabBarButtons![3], attribute: .left, multiplier: 1, constant: 0)
            ])
        // item 4
        tabBarButtons![3].translatesAutoresizingMaskIntoConstraints = false
        self.view.addConstraints([
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .top, relatedBy: .equal,
                               toItem: tabBar, attribute: .top, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .bottom, relatedBy: .equal,
                               toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .right, relatedBy: .equal,
                               toItem: tabBar, attribute: .right, multiplier: 1, constant: 0),
            NSLayoutConstraint(item: tabBarButtons![3], attribute: .width,  relatedBy: .equal,
                               toItem: tabBar, attribute: .width,  multiplier: 0.3333, constant: 0)
            ])
        // ios - How to definitively set UITabBar background color and UITabBar tint color - Stack Overflow
        // https://stackoverflow.com/questions/37626377/how-to-definitively-set-uitabbar-background-color-and-uitabbar-tint-color
        self.tabBar.barTintColor = .systemBackground
    }
    // 「小ネタ:UITabBarControllerに「モーダル表示するボタン」を追加する(Swift) - Qiita」
    // <https://qiita.com/paming/items/a1413480358fa81728cf>
    override func viewDidLayoutSubviews() {
        let tabBarButtons = self.tabBarButtons()
        UIView.setAnimationsEnabled(false)
        // item 1
        tabBarButtons![0].alpha = 0.0
        tabBarButtons![0].backgroundColor = .red
        tabBarButtons![0].layoutIfNeeded()
        // item 2
        tabBarButtons![1].backgroundColor = .cyan
        tabBarButtons![1].layoutIfNeeded()
        // item 3
        tabBarButtons![2].backgroundColor = .green
        tabBarButtons![2].layoutIfNeeded()
        // item 4
        tabBarButtons![3].backgroundColor = .yellow
        tabBarButtons![3].layoutIfNeeded()
        UIView.setAnimationsEnabled(true)
    }
}

extension TabBarController: UITabBarControllerDelegate {
    // 「swiftでUITabBarの特定のタブをタップした時にモーダル - Qiita」
    // https://qiita.com/higan96/items/5ea742b59a48a34baa32
    internal func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        self.viewDidLayoutSubviews()
        return true
    }
    // 「[Swift]タブ切り替え時に切り替え先のメソッドを実行する」
    // https://nobuhiroharada.net/2018/04/06/change-tab/
    internal func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
    }
}

// 「Swift - UITabbarControllerにモーダル表示するボタンを追加(68046)|teratail」
// <https://teratail.com/questions/68046>
extension TabBarController {
    func tabBarButtons() -> [UIView]? {
        return self.tabBar.subviews.reduce([], {
            (ret: [UIView], item:AnyObject) -> [UIView] in if let v = item as? UIView {
                if v.isKind(of: NSClassFromString("UITabBarButton")!) {
                    return ret + [v]
                }
            }
            return ret
        })
    }
}
■ タブボタンにアクセスするには?

これが無いと何も変更できません…。

  1. 以下の記事を参考に、tabBarButtons() を作成します。
■ タブボタンのサイズを個別に変更するには?
  1. 以下の記事を参考に、viewDidLoad() で各タブボタンに Constraints を設定します。
■ タブボタンの背景色を個別に変更するには?
  1. 以下の記事を参考に、viewDidLoad() でタブバーの背景色を設定します。
  2. 以下の記事を参考に、viewDidLayoutSubviews() を作成し、初期表示時のタブボタンの背景色を設定します。
  3. 以下の記事を参考に、tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) を作成し、タブ選択時のタブボタンの背景色を設定します。
■ どのタブも初期選択されない様するには?

これまでの設定の応用になります。

  1. 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
  2. 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。

99.ハマりポイント

  • 結構ハマったと様に思いますが…、ずいぶん前にやったので覚えてません…。

???

XX.まとめ

他にも色々とカスタマイズ出来るかもしれませんね♪

以下、GitHub にも UP してますので、参考になれば♪

?‍♂️?‍♂️?‍♂️

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

iOS 9 の端末でIn-Houseがダウンロードできない時メモ

iOSアプリをInHouseで配布したのでいざiOS9端末で取ろうとしたら
「アプリ名 は現在ダウンロードできません」 のダイアログが出たのでその原因メモ

結論

InHouseのipa置き場に置く、plistファイルのbundleIDが実際のものと違っていた

原因

ipaのbundleId
jp.bizen.ios.exemple.app-enterprise

InHouse plist
jp.bizen.ios.exemple.app-e

開発途中でbundleIdの管理方法が変わってしまったため起きた。
Xcodeに繋いでログを見ながらインストールしないと気づかなかったです :weary:

Xcode 11 でインストール失敗のログを読む

  1. Xcodeに端末を接続
  2. 上部 [Window] -> [Devices and Simulators] -> [Open Console]
  3. 右上のフィルタに Failed と入れる
  4. ダウンロードを開始する

ずらずら出てくるが

BundleValidator: Failed bundleIdentifier: jp.bizen.ios.exemple.app-e does not match expected bundleIdentifier: jp.bizen.ios.exemple.app-enterprise

 [ApplicationWorkspace]: Failed to install application: jp.bizen.ios.exemple.app-e; /var/mobile/Media/Downloads/-..../-....; Error Domain=SSErrorDomain Code=143 "(null)"

こんな感じで出てきます。

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

GameControllerを使ってDualshock4でUIViewを操作してみる

iOS13から、iPhoneでPS4やXboxのコントローラーが利用可能になりました。
コントローラーに対応したアプリで利用することができます。

どんなふうに使うのか試しに触ってみました。

コントローラーと接続する

コントローラーの接続は以下の2つのNotificationでハンドリングすることができます。
GCControllerDidConnect - NSNotification.Name | Apple Developer Documentation
GCControllerDidDisconnect - NSNotification.Name | Apple Developer Documentation

これらを使うには GameController をimportする必要があります。

import GameController

NotificationCenterを登録します。

NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleControllerDidConnect),
                                       name: .GCControllerDidConnect,
                                       object: nil)
NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleControllerDidConnect),
                                       name: .GCControllerDidDisconnect,
                                       object: nil)

これでコントローラーとの接続が確立したときと、切断されたときにそれぞれ通知を受け取ることができます。

コントローラーの入力をハンドリングする

コントローラの入力のハンドリングは GCExtendedGamepad - Game Controller | Apple Developer Documentation を使います。
これは Notification#object から取得することができます。

@objc func handleControllerDidConnect(notification: Notification) {
    guard let controller = notification.object as? GCController,
        let gamepad = controller.extendedGamepad else {
        return
    }
    ・・・
}

GCExtendedGamepad はコントローラーの入力値や入力に対するハンドラを持っており、それぞれ以下のプロパティでアクセスできます。
※一部ボタンは省略しています。

現在値を取得する場合は

// ×ボタンが押されているかどうか
let isButtonAPressed = extendedGamepad.buttonA.isPressed
// 右スティックの水平方向の傾き(-1~1)
let rightThumbstickX = extendedGamepad.rightThumbstick.xAxis.value
// 右スティックが上方向に傾けられているかどうか
let isRightThumbstickUpPressed = extendedGamepad.rightThumbstick.up.isPressed

入力を受け取ったタイミングでハンドリングしたい場合は

// ○ボタンが押された
gamepad.buttonB.pressedChangedHandler = { (input, value, isPressed) in
    ・・・
}
// OPTIONSボタンが押された
gamepad.buttonMenu.pressedChangedHandler = { (input, value, isPressed) in
    ・・・   
}
// 右スティックが傾けられた
gamepad.rightThumbstick.valueChangedHandler = { (pad, xAxis, yAxis) in
    ・・・      
}

というように使います。

UIViewを操作してみる

以下のようなサンプルを作ってみました。

  • ランダムな UIColor のリストを表示し、セルをタップするとその色のHEX値をアラートで表示
    • 左スティックでTableViewをスクロール
    • 右スティックでポインターを動かす
    • ○ボタンでセルを選択
  • OPTIONSボタンで配列の中身を新しくする

https://github.com/yoshimin/DualshockSample

dualshock.gif

コントローラーとiPhoneの接続は以下のように行います。

  1. iPhoneのBluetoothをONにしておく
  2. SHAREボタンを長押ししながら、ライトバーが点滅するまでPSボタンを長押しする
  3. Bluetoothの設定画面上にDUALSHOCKが表示されるのでタップするとペアリングされます

この状態でアプリを起動するとすぐに GCControllerDidConnect が呼ばれ入力を受け取ることができます。

簡単ですが実装してみた所感を以下にまとめます。

スクロールやポインターの移動など継続的な動きは現在値を使う

最初右スティックを使ってポインターを動かすのに rightThumbstick.valueChangedHandler を使ってみたのですが、これは値が変化したときにのみ発火するものなので、傾けたままの場合、最初に傾けたあとポインタが動かず止まってしまいました。
なので、 GCControllerDidConnect の通知で受け取った GCExtendedGamepad を保持しておき CADisplayLink を使ってフレームごとに leftThumbstick.xAxis leftThumbstick.yAxis を見てポインタを動かすようにしました。

逆に、ボタンの入力などその時々で処理したいイベントは pressedChangedHandler などのハンドラーを使います。

ボタンの押下イベントは isPressed をみる

closure の引数に isPressed があるので容易に想像できるかも知れませんが、ボタンの入力は押したときと離したときの2回呼ばれます。
GCControllerButtonValueChangedHandler

最初なにも気にせずに pressedChangedHandler の中に処理を書いていたら、処理が2回呼ばれていました...
ボタンが押し込まれたときに処理したい場合は isPressed == true であることを確認する必要があります。

まとめ

ゲームに使うものと思っていましたが、UIKit製のアプリでも普通に使えるのだなと思いました。
コントローラーの入力が受け取れるだけなので、それを受け取ってどうするかは実装次第で、何でもできます。
とはいえゲーム以外の需要はほとんどないと思いますが、普段PS4でAmazonビデオやNetflixで見ている私としてはiPadで動画アプリで視聴しているときに再生/一時停止/早送り/巻き戻しとかをコントローラーで操作できるのもありな気がしました。

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

iOS アプリ公開 スクリーンショットでハマった

s.png

以下のようなエラーができてた

無効なGeoJSON:ルーティング App カバレッジファイルが無効です。詳細については、次のガイド「Location and Maps Programming Guide」を参照してください。

原因: Choromeを使っているせい
解決策:safariを利用する

イメージにアルファチャンネルや透過を含めることはできません。

Photoshopでpngを普通に保存したら出てきた、どうしよう??
メニュー
⇨ ファイル
⇨ 書き出し
⇨ 書き出し方式
⇨ 透明部ボタンチェックボックスを外す
⇨ 書き出し
スクリーンショット 2019-10-16 14.13.43.png

メニュー
⇨ ファイル
⇨ クイック書き出し形式 の部分で 透明部分のチェックを外すでも可
スクリーンショット 2019-10-16 14.13.14.png

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

iOS申請→拒否られApple Connectへ修正バージョンを再アップしたが反映されない

拒否られたあと、Apple Connectへ新しいバージョンのAppをアップロードするが反映されない時

いつまで経っても反映されないと、バージョンを変えてみたりして再度アップロードするが、反映されない。。。

今回の場合、拒否られたバージョンが、ビルドに赤く拒否られた状態で残っていたので、反映されなかったようです。

拒否られたバージョンをビルドから取り外し(削除)したら、やっと反映されました

なぜ、反映されないのか、教えてくれてもよさそうなのに、と思ってしまいます。

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

ホーム画面に追加したspaサイトで遷移すると上部にバーが出るのを消す

iPadでspaで簡単なサイトを作って「ホームに画面に追加」をして疑似アプリケーション化して使っていたのですが、いつのまにか遷移すると「上部にバー」が出るようになってしまいました。「完了」を押すとバー自体は消えるのですが、せっかくの専用端末化が台無しです。ということで調査しました。

IMG_0002.jpg
※縮めています&ダークモードなので灰色。

環境

  • iPadOS 13.1.3

たぶん、どこかのバージョンで治る気がしますが...。

方法

結論から書くとmanifestファイルを追加します。

index.html
<!-- タイトルバーを消す設定 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">

<!-- 追加 -->
<link rel="manifest" href="manifest.json">
manifest.json
{  
  "name": "サンプルアプリ",  
  "short_name": "サンプル",  
  "display": "standalone",  
  "scope": "/",  
  "start_url": "/"  
}

nuxtの場合

  • manifest.jsonをstaticに入れます
  • nuxt.config.jsに以下を追加します。
nuxt.config.js
  head: {
    ...
    meta: [
      ...
      { name: 'apple-mobile-web-app-capable', content: 'yes' },  
      { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }
      ...
    ]
    ...
    link: [
      { rel: 'manifest', href: '/manifest.json'},
    ]
  }
  ...

メモ

前はmanifest無くてもできていたと思うのですが...。ハマったのでメモしておきます。

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

SDWebImage5にcacheMemoryOnlyオプションがいない

概要

SDWebImageを4系から5系にアップデートする必要があったのですが、
ディスクキャッシュを使用しないcacheMemoryOnlyオプションがoptionsから消えたようです。
contextが使えない方向けのピンポイントなマイグレーションガイドです。

経緯

StackOverFlowで秒殺だと思ったのですが、意外に簡潔なSwift用サンプルが見つからず。

公式GitHubで同じ質問をしてる人がいましたが、ちょっと分かりづらい気がしたので記事にしました。
https://github.com/SDWebImage/SDWebImage/issues/2729

一応、issueにある回答としては以下になります。

imageView.sd_setImage(with: url, 
  placeholderImage: nil, 
  context: [.storeCacheType : SDImageCacheType.memory.rawValue])

contextに辞書でいれるだけですね。
ただ、ホントにrawValueまで記載するの?と気になることばかりです。

reference

これだけだとPRを通す根拠に欠けるのでリファレンスを探しました。

SDWebImage - 5.0 Migration guide

https://github.com/SDWebImage/SDWebImage/wiki/5.0-Migration-guide

SDWebImageOptionsの項目から引用

SDWebImageCacheMemoryOnly removed, use @{SDWebImageContextStoreCacheType : @(SDImageCacheTypeMemory)} context option instead. For Swift user, using .storeCacheType: SDImageCacheType.memory.rawValue.

冗長に見えますが先程の回答の実装で問題なさそうです。

Migrationつまづきポイント

SDWebImage4系のときのコード▼

imageView.sd_setImage(with: url, 
  placeholderImage: UIImage(named: "hoge"), 
  options: [.cacheMemoryOnly, .retryFailed], 
  completed: {(image, error, ctype, url) in

5系だとこんな感じか?▼

imageView.sd_setImage(with: url, 
  placeholderImage: UIImage(named: "hoge"), 
  options: [.retryFailed], 
  context: [.storeCacheType: SDImageCacheType.memory.rawValue], 
  completed: {(image, error, ctype, url) in

上記にするとコンパイルエラーで変更案が表示され、従うとcontextがprogressに...
→ progressに辞書を渡してももちろんダメ:frowning2:

原因

ライブラリの中を追えば分かるのですが、UIImageView+WebCache.hを見ると
sd_setImage()メソッドは、placeholderImage, options, progress, completed全ての組み合わせを網羅しているわけではありません。
一番近いオーバーライドが以下だったので、contextがprogressに提案されたのでしょう。

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

以下を利用すれば大丈夫そうですね。

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

修正したコード ▼

imageView.sd_setImage(with: url, 
  placeholderImage: UIImage(named: "hoge"), 
  options: [.retryFailed], 
  context: [.storeCacheType: SDImageCacheType.memory.rawValue], 
  progress: nil,
  completed: {(image, error, ctype, url) in

おわり

完成されたライブラリだと思っていたので、ここにきてなかなかの変更ですね!
SDWebImageのポテンシャルをフル活用するのだけが目的のアプリつくるのも楽しそうですw

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