- 投稿日:2019-11-26T23:27:38+09:00
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/VIPERBook1SamplesUseCaseとは?
この記事でのUseCaseの前提として、
- システムを水平レイヤに分割したときのビジネスロジックを実装するもの
- 単一の目的を持ったコンポーネント
としてます。
この前提は、書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」での"ユースケース"から参考にしました。
注文入力システムに注文を追加するユースケースは、注文を削除するユースケースと比べると、明らかに異なる頻度と理由で変更される。ユースケースはシステムを分割する自然な方法である。また、ユースケースは、システムの水平レイヤーを薄く垂直にスライスしたものである。それぞれのユースケースは、UIの一部、アプリケーション特有のビジネスルールの一部、アプリケーションに依存しないビジネスルールの一部、データベース機能の一部を使用する。したがって、システムを水平レイヤーに分割するときには、それらを薄く垂直にユースケースとしても分割するのである。
ここまでで述べているのは、まず2つのユースケースがあるということ、そしてその分割方向についてです。
そして同書籍の中でユースケースの単位について「注文追加」「注文削除」を少し掘り下げています。それを読むと注文追加が
addOrder
で注文削除がdeleteOrder
とのことユースケースがお互いに切り離されていれば、addOrder(注文追加)のユースケースにフォーカスしたチームがdeleteOrder(注文削除)のユースケースにフォーカスしたチームの邪魔をする可能性は低い。
一応書いておくんですが、ユースケースは2つあるとは書いてあるもののそれらは2つのclassやstructとは書いてないんですね。むしろ
addOrder
とdeleteOrder
というのがメソッドにすら見える。そうなると「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>もう少し細かい特徴は
- 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もあります。
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をそのまま使うのではなく、
AnyUseCase
classの引数にしつつ抽象的な扱いをするようにしたいわけです。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を作ってますか?
- 投稿日:2019-11-26T23:14:01+09:00
プロジェクト起動したら画面が真っ黒になった。"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.storyboard
をMove to Trash
して
Home.storyboard
を新たに作成したらちゃんと読み込んでくれましたポイント
is Inisital ViewController
はチェック入れてた
- 投稿日:2019-11-26T19:56:56+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の3回目です。
今回は、位置情報を取得します。
位置情報の取得
CoreLocationをインポート
- 画面左側のファイルツリーから[ViewController.swift]を選択し、画面中央に表示されるエディタで、以下のように修正
- 【なぜ?】
- 位置情報ライブラリ(CoreLocation)を使うことを宣言することで、位置情報取得のプログラムを記述できるようになる
ViewController.swiftimport UIKit import CoreLocation // この行を追加 class ViewController: UIViewController {CoreLocation 用の変数を追加
- エディタで、以下のように修正
- 【なぜ?】
- この変数を通してプログラムで位置情報を取り扱うため
ViewController.swiftclass ViewController: UIViewController { var locationManager: CLLocationManager! // この行を追加 override func viewDidLoad() {CoreLocation の delegate の使用を宣言(プロトコルの継承)
- エディタで、以下のように修正
- 【なぜ?】
- プロトコルを継承することで、位置情報更新などのイベントに対する処理をプログラムすることができるようになる
- 今回は ViewController.swift で継承したことで、クラスが増えずシンプルな実装とすることができた
- (今回はしないが)別クラスで継承・実装すれば再利用性の高いクラスとできると思う
ViewController.swiftclass ViewController: UIViewController, CLLocationManagerDelegate { //この行を修正 var locationManager: CLLocationManager! override func viewDidLoad() {CoreLocation 変数の初期化とCoreLocationの初期処理
- エディタで、以下のように修正
- 【なぜ?】
- 変数は、初期化しないと使えない
- 位置情報更新時のイベントを処理するプログラムを指定する必要がある
- 明示的に位置情報取得開始を指示する必要がある
- 利用者から、このアプリで位置情報を使う許可をもらう必要がある
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // この行↓から locationManager = CLLocationManager() // 変数を初期化 locationManager.delegate = self // delegateとしてself(自インスタンス)を設定 locationManager.startUpdatingLocation() // 位置情報更新を指示 locationManager.requestWhenInUseAuthorization() // 位置情報取得の許可を得る // この行↑までを追加 }位置情報更新時に呼び出される処理を記述
- エディタで、以下のように修正
- 【なぜ?】
- 位置情報更新時に呼び出される処理
- 最新(last)の位置情報から緯度経度を取り出している
- 地図と連携する場合は、最新の位置を用いて地図を更新すればよいはず
ViewController.swiftoverride 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) } // この行↑までを追加位置情報使用許可文字列を設定
- 画面左側のファイルツリーから[MyGpsMap]-[Info.plist]を選択
- Ctrl+クリックでコンテキストメニューを表示し[Open As]-[Source Code]を選択(↓のようにテキストエディタ形式で表示される)
- ファイルの後ろ付近に以下の行を追加
- 【なぜ?】
- 利用許可画面に表示される文字列
- この文字列設定が存在しないと、利用許可を完了できず位置情報が取得できない
Info.plist</array> <!-- ここ↓から追加 --> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>常に位置情報の利用を許可する理由/目的を書く</string> <key>NSLocationWhenInUseUsageDescription</key> <string>アプリ起動時に位置情報の利用を許可する理由/目的を書く</string> <!-- ここ↑まで追加 --> </dict> </plist>テスト実行
今回の到達点
- 位置情報(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) } }連載
- 投稿日:2019-11-26T17:55:58+09:00
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の作業とか毎年忘れますね。
- 投稿日:2019-11-26T15:52:25+09:00
UITableViewController の タブボタンをカスタマイズしてみる
0.はじめに
UITableViewController のタブボタンを色々とカスタマイズしたかったので、やってみました。
試してみたのは、以下。
- タブボタンのサイズを個別に変更する
- タブボタンの背景色を個別に変更する
- どのタブも初期選択されない様にしてみる
ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。
本当は、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 }) } }■ タブボタンにアクセスするには?
これが無いと何も変更できません…。
- 以下の記事を参考に、
tabBarButtons()
を作成します。■ タブボタンのサイズを個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
で各タブボタンに Constraints を設定します。■ タブボタンの背景色を個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
でタブバーの背景色を設定します。- 以下の記事を参考に、
viewDidLayoutSubviews()
を作成し、初期表示時のタブボタンの背景色を設定します。- 以下の記事を参考に、
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
を作成し、タブ選択時のタブボタンの背景色を設定します。■ どのタブも初期選択されない様するには?
これまでの設定の応用になります。
- 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
- 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。
99.ハマりポイント
- 結構、ハマったと思いますが…、結構前にやったので覚えてません…。
???
XX.まとめ
他にも色々とカスタマイズ出来るかもしれませんね♪
以下、GitHub にも UP してますので、参考になれば♪
?♂️?♂️?♂️
- 投稿日:2019-11-26T15:52:25+09:00
UITabBarController の タブボタンをカスタマイズしてみる
0.はじめに
UITabBarController のタブボタンを色々とカスタマイズしたかったので、やってみました。
試してみたのは、以下。
- タブボタンのサイズを個別に変更する
- タブボタンの背景色を個別に変更する
- どのタブも初期選択されない様にしてみる
ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。
本当は、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 }) } }■ タブボタンにアクセスするには?
これが無いと何も変更できません…。
- 以下の記事を参考に、
tabBarButtons()
を作成します。■ タブボタンのサイズを個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
で各タブボタンに Constraints を設定します。■ タブボタンの背景色を個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
でタブバーの背景色を設定します。- 以下の記事を参考に、
viewDidLayoutSubviews()
を作成し、初期表示時のタブボタンの背景色を設定します。- 以下の記事を参考に、
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
を作成し、タブ選択時のタブボタンの背景色を設定します。■ どのタブも初期選択されない様するには?
これまでの設定の応用になります。
- 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
- 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。
99.ハマりポイント
- 結構ハマったと様に思いますが…、ずいぶん前にやったので覚えてません…。
???
XX.まとめ
他にも色々とカスタマイズ出来るかもしれませんね♪
以下、GitHub にも UP してますので、参考になれば♪
?♂️?♂️?♂️
- 投稿日:2019-11-26T14:24:50+09:00
SceneDelegateの追加方法
フリーランスの永田です。
自作ライブラリーを
https://github.com/daisukenagata/BothSidesCameracocoacontrolsに公開したところ、
https://www.cocoacontrols.comiOSはSwiftUIの実装がスタンダードになっていたので、ViewControllerをContentViewのSwiftUIに変更しました。
スターを押していただけるとモチベーションに繋がるので、スターを押していただけるとありがたいです。
Xcode11~ で途中からSwiftUIに変更するためにSceneDelegateの追加方法を紹介いたします。
infoPlistのマーク箇所を追加、
SwiftUIのプロジェクトを参考願います。
階層にFileの追加
Preview ContentはSwiftUIのプロジェクトを参考願います。
ソースの変更 AppDelegate.swift
AppDelegate.swiftimport 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.swiftimport 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. } }
- 投稿日:2019-11-26T14:21:53+09:00
チームでiOSアプリを初めて制作した話!!
みなさん初めまして!!Swiftを絶賛勉強中のりゅーちゃんです。関西の学生団体Volareに所属しています!
Swift学習して半年ほど経過し、団体内で開発しているアプリの制作が一旦落ち着いたので記事にしようと思いました!
今回はアプリのまとめ記事になりますが技術的にハマったことなども随時更新していきます何を作っているの?
簡単にデモ動画を添えてアプリの説明をしていきたいと思います!!
アプリ作成の経緯なんですが団体内でアイディアソンをおこない、みんなで作りたいものを決めました。。。
そんな中で選ばれたのがこの案です!(スライドショーの貼り方が分からないので理解しにくくなってしまいますが書いて説明していきます)アプリ作成においてメインのターゲットは私達のような一人暮らしの大学生です
アプリの決定した経緯については、
自炊をする際に献立を考えるのが面倒であるが、既存のアプリは冷蔵庫にあるものを参照して検索をかけている
→既存のもののターゲットは家族をもつ人向けである好き。嫌いを入力して自動的に献立を提示してくれるアプリがあると便利じゃんと感じた!
→基本的に作り置きはあまりしない、好きなものを好きな時に作りたい(冷蔵庫に食材があまり入っていない)このような要点から作ることを決めました!(コンセプトが同じものがないなら作ってしまおう)
需要があればここについて詳しく記述していきます!!デモ動画↓
なんとか形にすることができました!!
— 奥田りゅーや@クソザコイモムシ (@hd1J9rVe3xVFeU3) November 26, 2019
あとは細かいところをつめてリリース? pic.twitter.com/QfcA3gZAKqチーム構成、機能実装について
サーバーサイド、デザイン、iOSの領域に別れて開発を進めていきました!
サーバサイド機能実装について(後に詳しい機能について追記します)
- ユーザ認証機能
- デフォルトの食材一覧の設定
- 好き、嫌いの値による項目のランダム出力
iOSm機能実装
- Alamofireを使用したAPI通信(get,put,post)
- MVC設計
- コンフリクトが生じないようにXibを使い画面を作成する
- デザインに準拠したアプリの画面作成(delegateの理解)
ハマったものは別の記事に詳細を載せますがAPI通信部分についてはコードを提示しておきます(技術記事だし)
サインイン、サインアップについて
始めにViewControllerから見ていきましょう!!
RootViewController.swiftimport 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が一致した時、取得に画面が遷移する簡単な処理を実行しています!お次はModelです!!
RootViewModel.swiftimport 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.swiftimport Foundation struct User: Codable { let token: String }以上になります!!
今回に関してはクロージャー??、非同期??など理解が足りないためにハマった箇所がいくつもありましたがその部分に関しては別記事にまとめ発信していきます!!最後に
初めてアプリを作成、チームでの開発ということもあり開発の工数が予想以上に膨らんでしまいました。。。
苦しい期間が長かったですが実際に動いてくれているのを見ると我が子のようで可愛いですっ今回はiOSを担当しましたが今後はサーバーサイド の勉強を進めていきたいと感じています!!
最後まで読んでくださり、ありがとうございました
- 投稿日:2019-11-26T12:27:54+09:00
GameControllerを使ってDualshock4でUIViewを操作してみる
iOS13から、iPhoneでPS4やXboxのコントローラーが利用可能になりました。
コントローラーに対応したアプリで利用することができます。どんなふうに使うのか試しに触ってみました。
コントローラーと接続する
コントローラーの接続は以下の2つのNotificationでハンドリングすることができます。
GCControllerDidConnect - NSNotification.Name | Apple Developer Documentation
GCControllerDidDisconnect - NSNotification.Name | Apple Developer Documentationこれらを使うには
GameController
をimportする必要があります。import GameControllerNotificationCenterを登録します。
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
コントローラーとiPhoneの接続は以下のように行います。
- iPhoneのBluetoothをONにしておく
- SHAREボタンを長押ししながら、ライトバーが点滅するまでPSボタンを長押しする
- 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で動画アプリで視聴しているときに再生/一時停止/早送り/巻き戻しとかをコントローラーで操作できるのもありな気がしました。
- 投稿日:2019-11-26T10:05:56+09:00
トルツメにframeやらNSLayoutConstraint使うとかお前はミスターアンモナイトかwwwwww
初投稿です。温かい目でみてください。
現場でframeやらNSLayoutConstraintを変数化して弄ってトルツメを実現しているのが余りに多いので可読性を上げるためにもStackViewを使用して短いコードで書いちゃおうという記事です。
ログイン時と非ログイン時でレイアウトを変更したい!
例えばログイン時は会員メニューにアクセスできるが非ログイン時はそもそもメニューを出したくないなんてレイアウト
こんな感じですね。
そしてコードですがこちらです。
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にいれて自動でレイアウトを変更することによってトルツメを実現しています。
上部のラベルの制約ですが、
どの機種であっても幅を変えたくないので高さを30に固定。
上、右、左にsuperViewに0でConstraintを設定しています。下部のビューの制約ですが、
機種の大きさによって幅を動的に変えたいのでsuperViewの1/11の大きさに。
下、右、左にsuperViewに0でConstraintを設定しています。そして真ん中のビューですが、
上Constraintを上部ラベルに、
下Constraintを下部ビューに0で設定しています。これによって上部と下部がなくなった場合次にsuperViewのtopとbottomがくるので自動的に紐付けされhiddenのみでトルツメができるというわけです。
最後に
実はstackviewがめちゃくちゃ嫌いだったんですがトルツメするにあたってはこれ以上便利なものはないので好きになりました。
- 投稿日:2019-11-26T02:26:06+09:00
【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 { //日付が変わっていない時の処理をここに書く } }
- 投稿日:2019-11-26T01:23:00+09:00
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) in5系だとこんな感じか?▼
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に辞書を渡してももちろんダメ原因
ライブラリの中を追えば分かるのですが、
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