20191126のSwiftに関する記事は12件です。

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で続きを読む

プロジェクト起動したら画面が真っ黒になった。"Default Configuration" for UIWindowSceneSessionRoleApplication contained UISceneDelegateClassName key, but could not load class with name

状況

Tinderの擬似アプリ作ろうと久々に新規プロジェクト作成してビルドしたら

"Default Configuration" for UIWindowSceneSessionRoleApplication contained UISceneDelegateClassName key, but could not load class with name

こんなエラー出て画面が真っ黒に。。。。

対策

Main.storyboardMove to Trashして
Home.storyboardを新たに作成したらちゃんと読み込んでくれました

ポイント

  • is Inisital ViewControllerはチェック入れてた
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

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で続きを読む

SceneDelegateの追加方法

フリーランスの永田です。

自作ライブラリーを
https://github.com/daisukenagata/BothSidesCamera

cocoacontrolsに公開したところ、
https://www.cocoacontrols.com

iOSはSwiftUIの実装がスタンダードになっていたので、ViewControllerをContentViewのSwiftUIに変更しました。

スターを押していただけるとモチベーションに繋がるので、スターを押していただけるとありがたいです。

Xcode11~ で途中からSwiftUIに変更するためにSceneDelegateの追加方法を紹介いたします。

infoPlistのマーク箇所を追加、

SwiftUIのプロジェクトを参考願います。

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

階層にFileの追加

Preview ContentはSwiftUIのプロジェクトを参考願います。

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

ソースの変更 AppDelegate.swift

AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

ソースの追加 SceneDelegate.swift

SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.
    }


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

チームでiOSアプリを初めて制作した話!!

みなさん初めまして!!Swiftを絶賛勉強中のりゅーちゃんです。関西の学生団体Volareに所属しています!
Swift学習して半年ほど経過し、団体内で開発しているアプリの制作が一旦落ち着いたので記事にしようと思いました!
今回はアプリのまとめ記事になりますが技術的にハマったことなども随時更新していきます:bicyclist:

何を作っているの?

簡単にデモ動画を添えてアプリの説明をしていきたいと思います!!

アプリ作成の経緯なんですが団体内でアイディアソンをおこない、みんなで作りたいものを決めました。。。
そんな中で選ばれたのがこの案です!(スライドショーの貼り方が分からないので理解しにくくなってしまいますが書いて説明していきます:cry:

アプリ作成においてメインのターゲットは私達のような一人暮らしの大学生です:dark_sunglasses:
アプリの決定した経緯については、
自炊をする際に献立を考えるのが面倒であるが、既存のアプリは冷蔵庫にあるものを参照して検索をかけている
→既存のもののターゲットは家族をもつ人向けである

好き。嫌いを入力して自動的に献立を提示してくれるアプリがあると便利じゃんと感じた!
→基本的に作り置きはあまりしない、好きなものを好きな時に作りたい(冷蔵庫に食材があまり入っていない:ghost:

このような要点から作ることを決めました!(コンセプトが同じものがないなら作ってしまおう:relaxed:
需要があればここについて詳しく記述していきます!!

デモ動画↓

チーム構成、機能実装について

サーバーサイド、デザイン、iOSの領域に別れて開発を進めていきました!

サーバサイド機能実装について(後に詳しい機能について追記します:bow_tone1:)

  • ユーザ認証機能
  • デフォルトの食材一覧の設定
  • 好き、嫌いの値による項目のランダム出力

iOSm機能実装

  • Alamofireを使用したAPI通信(get,put,post)
  • MVC設計
  • コンフリクトが生じないようにXibを使い画面を作成する
  • デザインに準拠したアプリの画面作成(delegateの理解)

ハマったものは別の記事に詳細を載せますがAPI通信部分についてはコードを提示しておきます(技術記事だし:eyes:

:eyeglasses:サインイン、サインアップについて:eyeglasses:
始めにViewControllerから見ていきましょう!!
RootViewController.swift
import UIKit

final class RootViewController: UIViewController {
    static func make() -> RootViewController {
        let viewController = RootViewController()
        return viewController
    }

    private var model = RootViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        if  UserData.ID != nil {
            signInUser()
        } else {
            signUpUser()
        }
    }

//UUIDを生成→UserDefaults内に保存→UUIDを取り出し、Modelに処理を委託→token取得
    private func signUpUser() {
        let uuid = UUID().uuidString
        UserData.ID = uuid
        model.createUser(uuid: uuid, handler: { result in
            UserData.token = result.token
            self.showHomeViewController()
        })
    }

//tokenの取得→同じユーザーかの確認
    private func signInUser() {
        guard let uuid = UserData.ID else { return }
        model.loginUser(uuid: uuid, handler: { result in
            UserData.token = result.token
            self.showHomeViewController()
        })
    }

サインイン、サインアップの処理についてはViewの方でUUIDの保存、取得、tokenの保存、取得を行っています!
tokenが一致した時、取得に画面が遷移する簡単な処理を実行しています!:kissing_heart:

お次はModelです!!
RootViewModel.swift
import Foundation
import Alamofire

class RootViewModel {
    func createUser(uuid: String, handler: @escaping (User) -> Void) {
        let params: [String: Any] = [
            "uuid": uuid
        ]

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

        AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default)

            .validate(statusCode: 200..<300)
            .responseJSON{ response in
                switch response.result {
                case .success( _):
                    guard let data = response.data else { return }
                    guard let token = try? JSONDecoder().decode(User.self, from: data) else { return }
                    handler(token)
                case .failure(let error):
                    print(error)
                }
        }
    }
}

extension RootViewModel {
    func loginUser(uuid: String, handler: @escaping (User) -> Void) {
        let params: [String: Any] = [
            "uuid": uuid
        ]

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

        AF.request(url, method: .post, parameters: params, encoding: JSONEncoding.default)

            .validate(statusCode: 200..<300)
            .responseJSON{ response in
                switch response.result {
                case .success( _):
                    guard let data = response.data else { return }
                    guard let token = try? JSONDecoder().decode(User.self, from: data) else { return }
                    handler(token)
                case .failure(let error):
                    print(error)
                }
        }
    }
}

ModelについてはAlamofireを使用してViewから受け取ったUUIDを使いサーバーさんと通信しtokenを取得しています!

最後にCodableまわりです!
APIResponse.swift
import Foundation

struct User: Codable {
    let token: String
}

以上になります!!
今回に関してはクロージャー??、非同期??など理解が足りないためにハマった箇所がいくつもありましたがその部分に関しては別記事にまとめ発信していきます!!

最後に

初めてアプリを作成、チームでの開発ということもあり開発の工数が予想以上に膨らんでしまいました。。。
苦しい期間が長かったですが実際に動いてくれているのを見ると我が子のようで可愛いですっ:relaxed:

今回はiOSを担当しましたが今後はサーバーサイド の勉強を進めていきたいと感じています!!
最後まで読んでくださり、ありがとうございました:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

トルツメにframeやらNSLayoutConstraint使うとかお前はミスターアンモナイトかwwwwww

初投稿です。温かい目でみてください。

現場でframeやらNSLayoutConstraintを変数化して弄ってトルツメを実現しているのが余りに多いので可読性を上げるためにもStackViewを使用して短いコードで書いちゃおうという記事です。

ログイン時と非ログイン時でレイアウトを変更したい!

例えばログイン時は会員メニューにアクセスできるが非ログイン時はそもそもメニューを出したくないなんてレイアウト

圧縮用.gif

こんな感じですね。

そしてコードですがこちらです。

viewcontroller.swift
    @IBAction func eventButton(_ sender: Any) {
        UIView.animate(withDuration: 0.3) {
            self.hiddenLabel.isHidden = !self.hiddenLabel.isHidden;
            self.hiddenView.isHidden = !self.hiddenView.isHidden;
        }
    }
viewcontroller.m
- (IBAction)eventButton:(id)sender {
    [UIView animateWithDuration:0.3f animations:^{
        self.hiddenLabel.hidden = !self.hiddenLabel.isHidden;
        self.hiddenView.hidden = !self.hiddenView.isHidden;
    }];
}

以上です。

viewやらラベルをhiddenにするだけでトルツメできてます。

方法

StackViewにいれて自動でレイアウトを変更することによってトルツメを実現しています。
スクリーンショット 2019-11-26 7.45.18.png

上部のラベルの制約ですが、
スクリーンショット 2019-11-26 7.46.06.png
どの機種であっても幅を変えたくないので高さを30に固定。
上、右、左にsuperViewに0でConstraintを設定しています。

下部のビューの制約ですが、
スクリーンショット 2019-11-26 7.49.27.png
機種の大きさによって幅を動的に変えたいのでsuperViewの1/11の大きさに。
下、右、左にsuperViewに0でConstraintを設定しています。

そして真ん中のビューですが、
スクリーンショット 2019-11-26 7.53.42.png
上Constraintを上部ラベルに、
下Constraintを下部ビューに0で設定しています。

これによって上部と下部がなくなった場合次にsuperViewのtopとbottomがくるので自動的に紐付けされhiddenのみでトルツメができるというわけです。

最後に

実はstackviewがめちゃくちゃ嫌いだったんですがトルツメするにあたってはこれ以上便利なものはないので好きになりました。

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

【Swift】UserDefaultsを使って、日付更新ごとに処理する方法

UserDefaultsとは?

UserDefaultsとは簡単に言うとキーバリュー型のDB(データベース)にアクセスするために用いるもので、これを用いることで、アプリの起動時にキーと値のペアを永続的に保存することができます。その他の特徴を下記に記しておきます。

・< Key, Value >形式の辞書型でアクセス可能。
・各iOSアプリは、1端末に対して1つのUserDefaultsを保持している。
・アプリを削除するとUserDefaultsも消えるため、同じアプリを再インストールしてもUseDefaultsはリセットされる。

UserDefaultsは以上のような特徴を持っていて、主な使用目的としてはユーザーの設定情報(Preferences)を永続的に保存することが目的として挙げられます。

日付更新ごとの処理

今回はこのUserDefaultsを使って、日にちが変わるごとに何かしらの処理を実行してくれるようなプログラムを組んでいきます。
下記が全体コードになります。

import UIKit

// userdefaultsを用意しておく
let UD = UserDefaults.standard

//日付判定関数
func judgeDate(){
    //現在のカレンダ情報を設定
    let calender = Calendar.current
    //日本時間を設定
    let now_day = Date(timeIntervalSinceNow: 60 * 60 * 9)
    //日付判定結果
    var judge = Bool()

    // 日時経過チェック
    if UD.object(forKey: "today") != nil {
         let past_day = UD.object(forKey: "today") as! Date
         let now = calender.component(.day, from: now_day)
         let past = calender.component(.day, from: past_day)

         //日にちが変わっていた場合
         if now != past {
            judge = true
         }
         else {
            judge = false
         }
     }
     //初回実行のみelse
     else {
         judge = true
         /* 今の日時を保存 */
         UD.set(now_day, forKey: "today")
     }

     /* 日付が変わった場合はtrueの処理 */
     if judge == true {
          judge = false
    //日付が変わった時の処理をここに書く
     }
     else {
      //日付が変わっていない時の処理をここに書く 
     }
}

まずUserDefaultsが使えるように定数に代入します。

// userdefaultsを用意しておく
let UD = UserDefaults.standard

そしてcalender定数に現在のカレンダー情報を代入、now_dayに現在の日本時間を代入、最後に日付判定を行うBool値の変数を定義します。

func judgeDate(){
    //現在のカレンダ情報を設定
    let calender = Calendar.current
    //日本時間を設定
    let now_day = Date(timeIntervalSinceNow: 60 * 60 * 9)
    //日付判定結果
    var judge = Bool()

次にif文でUserDefaultsのtodayキーに値があるか判定します。
あったらif文内の処理へ、なかったら後ほど解説の初回起動時の処理へ進みます。
if文内の処理ではまず最初にpast_dayにすでに保存してあった日時情報を代入し、nowに現在の日付を代入、そしてpastにpast_dayの日時情報を使って過去の日付を代入します。

 if UD.object(forKey: "today") != nil {
         let past_day = UD.object(forKey: "today") as! Date
         let now = calender.component(.day, from: now_day)
         let past = calender.component(.day, from: past_day)

次に先ほど代入したnowがpastと異なる場合はjudgeにtrueを、同一であればfalseを代入します。

if now != past {
            judge = true
         }
         else {
            judge = false
         }
     }

次に初回起動時の処理としてjudgeにtrueを代入し、現在日時をUserDefaultsに設定します。

 //初回実行のみelse
     else {
         judge = true
         /* 今の日時を保存 */
         UD.set(now_day, forKey: "today")
     }

最後にif文で初回起動時・日付変更時にする処理を記述し、else内で日付が変わっていない時にする処理を記述します。

 /* 日付が変わった・初回起動時の場合はtrueの処理 */
     if judge == true {
          judge = false
    //日付が変わった時の処理をここに書く
     }
     else {
      //日付が変わっていない時の処理をここに書く 
     }
}
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む