- 投稿日:2021-03-05T23:42:33+09:00
Swiftでの配列の初期化方法
Swiftでの配列の初期化
Swiftでの配列の初期化はいくつか方法があるためそれらを紹介していきます。
他にもあればコメントで教えてください。要素あり初期化
var array1 = [12, 32] var array2: [Int] = [12, 32] var array3: Array<Int> = [12, 32]要素なし初期化
var array1 = [Int].init() var array2: [Int] = [Int]() var array3: [Int] = [Int].init() var array4: Array<Int> = Array<Int>() var array5: Array<Int> = Array<Int>.init()
- 投稿日:2021-03-05T22:19:55+09:00
【Swift】MVVMとは??でモヤモヤしてる人向けの図解付き解説
前提
ある程度調べていてアーキテクチャという言葉の意味くらいならわかる
RxSwiftを自分なりに調べていて読めはするMVVM
まず、アプリの使われ方、あり方について考えてみる。
アプリを使うのはユーザーで、ユーザーはアプリを開き、
つまり画面を開き、操作を行う。
この操作に対して、ViewModelやModelのロジックが働き、
Viewはそれらのロジックの結果を画面に反映する。このように、何かのイベントに対して何かのロジックが働き、結果を反映する。
その流れの中で、表示非表示や変形、バリデーション、イベントに対するロジックなど
役割ごとに分け、責務の切り分けをするのがアーキテクチャであり、MVVMである。図解
View(UIロジック)
言うまでもなく、表示を担当する。
この部分は、ユーザーインターフェース層とも言われ、
ユーザーの目に触れる部分を担当している。
ViewControllerとViewがそれである。Storyboardと連携し、UIパーツの表示。(Viewの責務)
ユーザーからのタップ、入力イベントなどの操作をViewModelに送信し、
その結果がViewModelから流れてきたらUIコンポーネントや表示ロジックに反映させる。ViewModel(プレゼンテーションロジック)
View(ViewController)から送られてくるイベントや値の加工をするところ
入力イベントであればテキストを受け取り、文字数制限などの
モデルに書かれたバリデーションロジック関数を呼び出して
その結果をView(ViewController)に返す。Model(ビジネスロジック)
View(ViewController)、ViewModelが担当する以外の役割を担当する。
バリデーションロジックやAPIに関するEntity、Repository、
シングルトンなどのManagerなど、細かい責務がModelに切り出される。おわりに
実務で新規開発をして、ふわっとMVVMがわかったので備忘録として。
間違っている点があればコメントしていただけると幸いです。
- 投稿日:2021-03-05T21:23:38+09:00
TextFieldを隠さずにキーボードを表示させる方法-IQKeyboardManager
Keyboardが表示されて
TextField
が隠れてしまう時があると思います。
かと言って毎回Keyboardの高さ分画面を上にずらすと、元から画面上部にあるTextField
が画面外に行ってしまいます。
TextField
の位置によって画面をずらすか判断する、ってこともできるかもしれませんが、毎回実装するのは面倒ですよね…。実はその問題、先人が既に解決してくれています✨
その方法がこちらのGithubで公開されています→IQKeyboardManager
とても簡単ですが、英語で書いてあるので実装方法をまとめてみました。実装方法
まずはターミナルで実装したいプロジェクトが入っているフォルダに移動します。
次のコマンドで同じフォルダ内にCocoaPodsファイルを作成します。
pod init
作成された
Podfile
を開き、次の記述を追加します。# Pods for xxx(プロジェクト名) pod 'IQKeyboardManagerSwift' ←追加追加できたら
Podfile
を一旦閉じ、ターミナルで次のコマンドを打ってPodfile
をInstallします。pod install
すると
.xcworkspace
(アイコンが白いプロジェクト)が作成されるので、これを開きます。
AppleDelegate.swift
ファイルを開き、次の箇所を追加します。import UIKit import IQKeyboardManagerSwift //←追加 @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? //←追加 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. IQKeyboardManager.shared.enable = true //←追加 return true }これで完了です!
あとは開いて動作を確認してみてください。まとめ
いかがでしたでしょうか。とっても簡単ですね。
これは非常に便利なので、使わない理由がありません!
是非是非ご活用ください〜(それにしても先人は偉大ですね…)
- 投稿日:2021-03-05T19:12:54+09:00
filterの使い方について簡単にまとめる
filterは配列のデータでいらないものを除去したりする際に使います。
使い方としてはこんな感じ
配列を取得
配列をfilterする
プログラムだとこんな感じ
let data: [テーブル名] = テーブルを取得する関数
data.filter({$0.レコード名 == 値})
$0 ← これはなんだと思う人が多いと思うんですが、
簡単に言うとテーブル名です。
なのでdata.filter({$0.レコード名 == 値})は日本語で伝えると
データ.フィルター({テーブル名のレコード = 値})
ループで回してifで値を追加していくみたいな処理も全然ありなんですけど、結構ソースも見づらいしループだらけで結構処理も重くなってしまうんですよね。
なんでfilterを使いこなせるようになると割と便利かもです。
とはいえ、いきなり初学者が覚えられるかと言われると割と難しいので、実践などを通してあーこういうことね
なんて思えるようになればいいかなと思います。
- 投稿日:2021-03-05T18:45:11+09:00
【Swift】アプリを再インストールした際にFCMのリモート通知が届かなくなった
環境
- Xcode Version 12.3 (12C33)
- iOS 13 以上
- Firebase/Messaging (7.1.0)
初期実装
AppDelegate
では以下のような初期化処理を行っていたAppDelegate.swiftimport FirebaseCore import FirebaseMessaging import UIKit // MARK: - AppDelegate @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { self.setupFirebase(application: application) let window = UIWindow() self.window = window return true } private func setupFirebase(application: UIApplication) { FirebaseApp.configure() Messaging.messaging().delegate = self application.registerForRemoteNotifications() }FCMトークンが必要な場面で以下のように呼び出しを行っていた
import FirebaseMessaging // 中略 let fcmToken = Messaging.messaging().fcmToken問題発覚
- アプリをアンインストール後再インストールするとリモート通知が届かなくなった
- サーバーのログを確認すると
"error":"No information found about this instance id."
とエラーが出ていた原因
- 再インストール後
Messaging.messaging().fcmToken
を利用するとアンインストール前の FCM トークンが返却されていた対応
Messaging.messaging().token(completion:)
を一度呼び出すことでcompletion
呼び出し後には有効なトークンが取得できるようになるsetupFirebase(application:)
を以下の通り修正AppDelegate.swiftprivate func setupFirebase(application: UIApplication) { FirebaseApp.configure() Messaging.messaging().delegate = self // iOSは起動時に意図的にFCMトークンを取得する処理を追加することで「アプリが再インストール」された場合も対応できる Messaging.messaging().token { (_, error: Error?) in guard error == nil else { return } application.registerForRemoteNotifications() } }検証
検証1: アプリ再インストール時の挙動
MessagingDelegate
のfunc messaging(_ messaging:didReceiveRegistrationToken fcmToken:)
を以下の通り実装AppDelegate.swiftfunc messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { print("*** fcmToken: \(fcmToken ?? "")") print("*** fcmToken: \(Messaging.messaging().fcmToken ?? "")") }
didReceiveRegistrationToken
のfcmToken
とMessaging.messaging().fcmToken
が一致することは確認している
- 通知が届く状態のアプリをデバッグ実行し
didReceiveRegistrationToken
デリゲートメソッドに届いたFCMトークンを確認- FCMトークンが有効か curl コマンドで確認
- デバッグ実行を中止しアプリをアンインストール
- アプリをデバッグ実行(再インストール)し
didReceiveRegistrationToken
デリゲートメソッドに届いたFCMトークンを確認- FCMトークンが有効か確認
- 1分程度放置し
didReceiveRegistrationToken
デリゲートメソッドが再び発火するか確認- PUSH通知が届くか確認
- アプリを再起動(デバッグ実行)
didReceiveRegistrationToken
デリゲートメソッドに届いたFCMトークンを確認- FCMトークンが有効か確認
- 1分程度放置し
didReceiveRegistrationToken
デリゲートメソッドが再び発火するか確認- PUSH通知が届くか確認
結果
- トークンAを得る
- トークンAは curl 有効であることがわかる
- 実行可
- トークンAを得る
- トークンAは curl 無効であることがわかる(
"error":"No information found about this instance id."
)- 発火しない(トークンAは更新されない)
- 届かない
- 実行可
- トークンBを得る
- トークンBは curl コマンドより有効であることがわかる
- 発火しない(トークンBは更新されない)
- 届く
検証2: トークン取得メソッドを変更しエラーハンドリングを試みる
MessagingDelegate
のfunc messaging(_ messaging:didReceiveRegistrationToken fcmToken:)
を以下の通り実装AppDelegate.swiftfunc messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { print("*** fcmToken: \(fcmToken ?? "")") print("*** fcmToken: \(Messaging.messaging().fcmToken ?? "")") Messaging.messaging().token { (token: String?, error: Error?) in print("*** Messaging.messaging().token") print("*** fcmToken: \(token ?? "")") print("*** fcmTokenError: \(error?.localizedDescription ?? "nothing")") } }以下の手順を試みる
- 通知が届く状態のアプリをデバッグ実行し
didReceiveRegistrationToken
ログを確認- FCMトークンが有効か curl コマンドで確認する
- デバッグ実行を中止しアプリをアンインストール
- アプリを再インストール(デバッグ実行)し
didReceiveRegistrationToken
内部のログを確認結果
- 以下の通りトークンAを得る
*** fcmToken: [トークンA] *** fcmToken: [トークンA] *** Messaging.messaging().token *** fcmToken: [トークンA] *** fcmTokenError: nothing
- トークンA はcurl コマンドより有効であることがわかる
- 実行可能
- 以下のログを得る
*** fcmToken: [トークンA] *** fcmToken: [トークンA] *** fcmToken: [トークンB] *** fcmToken: [トークンB] *** Messaging.messaging().token *** fcmToken: [トークンB] *** fcmTokenError: nothing *** Messaging.messaging().token *** fcmToken: [トークンB] *** fcmTokenError: nothingcurl コマンドによる追加検証で以下が判明している
- 上記ログ2行目時点ではトークンAは有効
- 上記ログ3行目時点ではトークンAは無効
- トークンBは有効
検証結果まとめ
- アプリを再インストールした際に
didReceiveRegistrationToken
でアンインストール前に利用していたトークンが返却されるMessaging.messaging().fcmToken
ではdidReveiveRegistrationToken
で得られるトークンと同じものを取得するdidReceiveRegistrationToken
発火後何らかの原因でアンインストール前に利用していたトークンが無効化される- 無効化された後アプリを再起動することで正常に通信可能なトークンを
didReceiveRegistrationToken
、Messaging.messaging().fcmToken
で利用可能となるMessaging.messaging().token(completion:)
メソッドを用いることで再インストール後にも有効なFCMトークンを得ることができる- その際
Messaging.messaging().token(completion:)
メソッドにはエラーが入ってこないMessaging.messaging().token(completion:)
メソッドを呼び出すとdidReveiveRegistrationToken
が発火し、内部で有効なFCMトークンを得ることができるMessaging.messaging().token(completion:)
メソッドを呼び出した後にはMessaging.messaging().fcmToken
プロパティで有効なFCMトークンを得ることができる
- 投稿日:2021-03-05T16:15:50+09:00
Swiftのメモ
textField.resignFirstResponder()は、キーボーどを呼び出しした際に、returnキーで消えてくれるもの。もしくは、キーボード以外の画面をタッチすると、消えてくれる
- 投稿日:2021-03-05T16:15:50+09:00
Swiftのメモ(素人が趣味でやってるのをメモしてるやつ。自分用)
textField.resignFirstResponder()は、キーボーどを呼び出しした際に、returnキーで消えてくれるもの。もしくは、キーボード以外の画面をタッチすると、消えてくれる
dismiss(animated: true, completion: nil)で、画面遷移する前の画面に戻れる感じ?
performSegue(withIdentifier: "SegueにつけるID名", sender: nil)
これで指定したSegueから移行させることができるはず・・・できた一応
- 投稿日:2021-03-05T16:15:15+09:00
Swift 構造体よりクラスを採用したほうが良いケース
この記事について
この記事は、開発未経験の人間がインプットした内容が書かれています。
実際に開発経験を積まないと身につかない、かといって疎かにしたら後々痛い目にあう、といったジレンマを少しでも解消するために、「頭の中を言語化し、解釈違いを指摘してもらう」という手段を取ることとしました。
気になる点がありましたらツッコミをいただけると嬉しいです。記事の内容
Swiftで開発する際に、クラスを採用したほうが良いケースのまとめ。
インプット教材
書籍「Swift実践入門」第12章
開発環境
% swift -version Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) % xcodebuild -version Xcode 12.4Swiftの開発は構造体(struct)が推奨されている
Swiftでの開発は、想定しない参照によるバグがないことや安全性の高さから構造体での開発を推奨されている。
とはいえ、依然としてクラスのほうが良いケースも存在するらしい。そのケースを理解していれば、それ以外は構造体で組めば良いことになるので、クラスの採用基準を注目し、記事にしてみた。クラスを採用する基準
1.変更した値などをインスタンスに保持したいとき
例) タイマーアプリを実装する。ターゲットを構造体とクラスとで挙動を比較する。
- 参照元のプロトコルでターゲット先の型名"identifier"とカウント数"count"を定義。
- 拡張機能を使ってカウントを進める関数を実装。
- プロトコルに準拠した2つのターゲットを用意し、プロパティに値を設定。
- タイマーを実行する"Timer"を定義。今回は5秒。
- それぞれをインスタンス化しスタートを実行。終了後の"count"を確認。
protocol Target { var identifier: String { get set } var count: Int { get set } mutating func action() } extension Target { mutating func action() { count += 1 print("id: \(identifier), count: \(count)") } } struct ValueTypeTarget : Target { var identifier = "Value Type" var count = 0 init() {} } class ReferenceTypeTarget : Target { var identifier = "Reference Type" var count = 0 init() {} } struct Timer { var target: Target mutating func start() { for _ in 0..<5 { target.action() } } } //構造体のターゲットを登録してタイマーを実行 let valueTypeTarget: Target = ValueTypeTarget() var timer1 = Timer(target: valueTypeTarget) timer1.start() valueTypeTarget.count //クラスのターゲットを登録してタイマーを実行 let referenceTypeTarget = ReferenceTypeTarget() var timer2 = Timer(target: referenceTypeTarget) timer2.start() referenceTypeTarget.count出力結果
id: Value Type, count: 1 id: Value Type, count: 2 id: Value Type, count: 3 id: Value Type, count: 4 id: Value Type, count: 5 0 // コピーを作っただけなので値は共有されていない id: Reference Type, count: 1 id: Reference Type, count: 2 id: Reference Type, count: 3 id: Reference Type, count: 4 id: Reference Type, count: 5 5 // 値を共有している構造体は値型であるため、ターゲットのコピーがインスタンスになる。つまり、ターゲットのプロパティの値が変更されても(countの値が5に更新されても)、インスタンスの値はコピーした時点での値のままである。
対して、クラスは参照型なのでインスタンスの値はターゲットと共有している。スタート実行後に値を確認すると反映されているのがわかる。
タイマーアプリのような、実行後の値をインスタンスにも共有したい場合などにはクラスが適切である。2.デイニシャライザを実装したいとき
クラスにしかない機能にデイニシャライザがある。デイニシャライザは型がnilになった時に実行される機能である。
例) 一時ファイルを作成し、削除する。
- ファイルの状態を表現する変数を定義
- インスタンス生成時にファイル作成、インスタンス削除時にファイル削除を定義したクラスを定義
- ファイルを生成、削除を実行し挙動の確認
var temporaryData: String? class SomeClass { init() { print("Create a temporary data") temporaryData = "a temporary data" } deinit { print("Clean up the temporary") temporaryData = nil } } //一時ファイル作成 var someClass: SomeClass? = SomeClass() temporaryData // "a temporary data" // 削除 someClass = nil temporaryData // nil出力結果
Create a temporary data Clean up the temporaryインスタンスの値がnilになるとデイニシャライザが実行されデータが削除される。
メモリに確保されたインスタンスやプロパティはARC(Automatic reference counting)によって自動的に破棄されるので通常はデイニシャライザを記述する必要はないが、ファイルの操作をする場合、自動でファイルを閉じたりしてくれないので、確実に閉じるためにデイニシャライザを利用するケースがある。
3.複数の型でプロパティの中身を共有できる
例) Animalクラスがあり、それを継承したクラスが複数ある。
Animalクラスにはownerプロパティがあり、プロパティオブザーバ(値がセットされた時に実行される機能)が定義されている。継承したクラスのインスタンスのプロパティに値をセットしたい。class Animal { var owner: String? { didSet { guard let owner = owner else { return } print("\(owner) さんが飼い主になりました。") } } } class Bear : Animal {} class Tiger : Animal {} class WildEagle : Animal {} // 値をセットするとプロパティオブザーバが実行 let bear = Bear() bear.owner = "吉田沙保里"出力結果
吉田沙保里 さんが飼い主になりました。クラスの参照機能を使えば親クラスのプロパティを子クラスにも引き継ぐことができる。
これを構造型で実装しようとすると、少々面倒な処理になる。// プロトコルではプロパティの宣言しかできない。 protocol Ownable { var owner : String { get set } } // 継承した各クラスにプロパティの中身を書かなければならない。 struct Dog : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } struct Shark : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } struct Buffalo : Ownable { var owner: String { didSet { print("\(owner)さんが飼い主になりました。") } } } // インスタンス化時に別の値を入れてから値を更新する var buffulo = Buffalo(owner: "") buffulo.owner = "吉田沙保里"出力結果
吉田沙保里さんが飼い主になりました。プロトコルでストアドプロパティやプロパティオブザーバは定義できない仕様になっているので、参照先の構造体で個別に定義してあげないといけない。また、プロパティオブザーバを実行させるためにインスタンス化時にいったん別の値を入れないといけない。
全然スマートじゃないので、参照元でストアドプロパティやプロパティオブザーバを定義したい場合はクラス型が良い。まとめ
クラスを採用したほうが良いと判断する基準は、
- 参照先で更新されたプロパティをインスタンスにも共有させたいか
- デイニシャライザを使いたいか
- 参照元でプロパティを定義したいか。それを複数の参照先で共有したいか
になるのかなぁ、と書籍を読んで思いました。
- 投稿日:2021-03-05T16:09:23+09:00
【Swift】Compositional Layoutsで実現する疎結合な実装
はじめに
Compositional Layouts
がWWDC2019で発表され、ここ数ヶ月でようやくiOS13以上をターゲットにしたプロジェクトが増えてきたのではないでしょうか?
SwiftUI
を取り入れている技術の記事も目立ってきましたが、iOS14にならないと不自由も多く、最初から機能が豊富なCompositional Layouts
を選択するのも1つの判断かと思います。本記事では実際にプロジェクトに導入してみたので、どのような構成で導入してみたのかをまとめています。Compositional Layouts の優位性
そもそも、
Compositional Layouts
で組むことは、何がメリットなのかというお話をざっくりしておきます。1. UICollectionViewDelegateFlowLayout のデメリット
iOS12 以下で
UICollectionView
を用いて複雑なレイアウトを組む場合、こちらを検討する人が多いでしょう。
UICollectionViewDelegateFlowLayout
をViewController
に継承して、画面の設定を直接書いていきます。Example
final class ExampleViewController: UIViewController, UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { /* セルのサイズ */ } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { /* セルの間隔 */ } . . . }この実装にはいくつかの問題があります。その最も大きな問題は、実装する際に
ViewController
と密結合してしまうことです。どうして密結合になってしまうのかというと、それは継承の関連に問題があります。
UICollectionView
の実装ではUICollectionViewDelegate
の実装がほとんどの場合で必要になります。UICollectionViewDelegateFlowLayout
はそれを継承しているため、ViewController
から実装が剥がせないのです。※1これはViewControllerの肥大化にも繋がりよくありません。仮に
enum
やstruct
で設定を定数化して切り出したり、分岐処理を切り出すことはできても、呼び出し部分はどうしてもViewController
に残ってしまいます。※1:
UICollectionViewDelegate
(とさらに親のUIScrollViewDelegate
)のAPIを使用しないなら剥がすこともできますが、その場合はそもそも設計段階でUIScrollView
+UIStackView
を検討する方が適切な可能性があります。2. UICollectionViewLayout のデメリット
UICollectionViewLayout
を継承したカスタムクラスを作成する方法もあります。final class ExampleCollectionViewFlowLayout: UICollectionViewFlowLayout { override func prepare(){ super.prepare() // レイアウトなどの計算 } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { // IndexPathに応じたCellのAttributesを返す } . . . }1の時とは違い
ViewController
と独立して実装できるため疎結合な作りにすることができます。その反面として、APIのライフライクルがやや難しい部分もあり、実装が容易ではないという面をもちます。実際のところ、ほとんどのUIは1で事足りてしまうため、わざわざこちらで実装するのはオーバースペックなことが多く、疎結合にしたいがためにこちらで実装する、といったことを現場ではあまりしないのが実情でしょう。
3. Compositional Layoutsのメリット
ざっくり言えば、上であげた 1と2の良いとこどりできるよ! ってことになります。
- ①のように、ある程度決まった形で書ける
- ②のように、疎結合にできる
という点を兼ね備えています。
また、UIを組む上でも以前と比べてわかりやすくなったという点もあります。
具体的な説明に関しては、たくさん出回っているので説明はしません。
- 時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~今回は、疎結合な実装方法にフォーカスしていきます。
Compositional Layouts + MVP
今回は実装を考えたプロジェクトが
MVP
をベースとした設計のため、それを基本にコードの記載を行っていきますが、Clean Architecture
やVIPER
など、疎結合が実現可能なアーキテクチャーであれば、同じような形で実装を行うことができるでしょう。以下は、実際に運用しているアプリの構成を簡単にまとめたものです。
先に結論の概要から述べてしまうと、上の構成に
Compositional Layouts
を導入するとこのようになります。抽象的なプロトコルとして書き出すことで共通化し、
ViewController
に依存しないように切り出しています。実装
具体的なコードを見ていきます。
1. 通常の実装をする
想像しやすいように、通常の実装からどのように行うかを見ていきます。
以下は、Compositional Layouts
で複数のレイアウトを組む際の簡単な例です。final class ViewControlle: UIViewController { // MARK: Property private lazy var collectionView: UICollectionView = { let collection = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) /* ~ 略 ~ */ return collection }() private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { let layout = UICollectionViewCompositionalLayout { [weak self] (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in // Section番号でレイアウトの分岐 switch section { case 0: return self?.createLayoutA() case 1: return self?.createLayoutB() default: fatalError() } } return layout }() // MARK: Method private func createLayoutA() -> UICollectionViewCompositionalLayout { /* ~ 略 ~ */ return UICollectionViewCompositionalLayout(section: section) } private func createLayoutB() -> UICollectionViewCompositionalLayout { /* ~ 略 ~ */ return UICollectionViewCompositionalLayout(section: section) } }複数のレイアウトを構成したい場合は、レイアウトの数に合わせて、その設定メソッドが増えていきます。
Compositional Layouts
でも、普通に実装した場合はViewControllerの肥大化を招きます。この肥大化を防ぐために、分離していきます。
2. レイアウトの抽象化をする
上記のコードからも分かる通り、
UICollectionViewCompositionalLayout
ではレイアウトをSection番号で分岐できるため、この部分を抽象化して取り出すことで、すっきりとした書き方にすることができます。具体的には、以下のレイアウトがあった場合、図右側のような抽象化を行います。
セクションに共通する処理を整理して抽象化していきます。
例として、抽象化するとこのようになります。protocol SectionProtocol { // セクションのアイテム数 var numberOfItems: Int { get } // レイアウトの生成 func layoutSection(_ view: UIView) -> NSCollectionLayoutSection // セルの生成 func configureCell(_ view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell // セルタップ時 func selectItem(_ controller: ViewController, at indexPath: IndexPath) }各人の実装によって抽象化されるものは変わるかと思いますが、上記は大体共通して実装することになるでしょう。これはただ抽象化しただけではなく、画面を構成するモデルの役割もはたします。
このプロトコルを各セクションごとに継承し、セクションごとに設定を記載していきます。
struct SectionA: SectionProtocol { let numberOfItems = 1 func layoutSection(_ view: UIView) -> NSCollectionLayoutSection { /* 略 */ return UICollectionViewCompositionalLayout(section: section) } func configureCell(_ view: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { let cell = view.dequeueReusableCell(withReuseIdentifier: "your cell id", for: indexPath) as! SectionACell /* 略 */ return cell } func selectItem(_ controller: ViewController, at indexPath: IndexPath) { // do some action } }こうすることで、
ViewController
からレイアウト部分を、別クラスとして分離することができます。3. ViewControllerから分離する
別クラスとして分離したので、
ViewController
はこのようにすっきりとした形になります。final class ViewController: UIViewController { private var sections: [SectionProtocol] private lazy var collectionView: UICollectionView = { let collection = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) /* ~ 略 ~ */ return collection }() private lazy var compositionalLayout: UICollectionViewLayout = { return UICollectionViewCompositionalLayout { [weak self] section, _ in return self?.sections[section].layoutSection(self ?? .init()) // force cast でも問題ない } }() }また、抽象化した他のプロパティやメソッドは、以下のように呼び出すことができます。
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate { func numberOfSections(in collectionView: UICollectionView) -> Int { sections.count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { sections[section].numberOfItems } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { sections[indexPath.section].configureCell(collectionView, at: indexPath) } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { sections[indexPath.section].selectItem(self, at: indexPath) } }抽象化されているため、分岐の処理がなくとても綺麗な実装になっています。
4. MVP構成にする
抽象化したセクションのモデル一覧である
private var sections: [SectionProtocol]を
ViewController
からPresenter
に移行するだけです。この部分はMVP構成にするだけなので、構成の仕方に関して記載は致しません。後述のリポジトリを見ていただけると幸いです。
その他
セクション側でアクションの処理を行いたい場合は、
ViewController
からPresenter
をフックしてあげることで、単一方向な処理を実現することができます。具体例として、セルをタップしたい際の挙動をあげておきます。
protocol Presentable: AnyObject { var sections: [SectionProtocol] { get } func selectItem(at indexPath: IndexPath) } final class Presenter: Presentable { private var sections: [SectionProtocol] func selectItem(at indexPath: IndexPath) { // do someting } }final class ViewController: UICollectionViewDelegate { private(set) var presenter: Presentable! /* 略 */ func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { presenter.sections[indexPath.section].selectItem(self, at: indexPath) } }struct SectionA: SectionProtocol { /* 略 */ func selectItem(_ controller: ViewController, at indexPath: IndexPath) { controller.presenter.selectItem(at: indexPath) } }先ほどの図に照らし合わせると、
このような関係になっています。
このように、抽象化して分離することで、
ViewController
の肥大化を防ぎつつ、疎結合な作りを実現することができるのです。終わりに
少しコードが多くなってしまい、わかりづらい部分もあるかもしれません。
動作するリポジトリを置いておくので、こちらからコードを読んでいただけると幸いです。
- CompositionalLayouts-MVPまた、この実装は
Compositional Layouts
をiOS12以下で使用するためのバックポートライブラリである
- kishikawakatsumi / IBPCollectionViewCompositionalLayoutをベースにしています。
そちらをみていただくと、より理解を深めることができます。
ご指摘などありましたらコメントいただけると幸いですmm
- 投稿日:2021-03-05T15:56:30+09:00
[Xcode] Storyboard Referenceを使用してStoryboardを分割する
目次
はじめに
環境
今回作成するサンプル
Storyboard Referenceを作成する
[補足] TabBarのアイコンやタイトルを変更する
最後にはじめに
Storyboard Reference
は、他のStoryboardを参照してStoryboardを分割できるものです。Segueで異なるStoryboard間をつなげることもできます。
個人アプリ開発の際に、ViewControllerやSegueの数が増えてきたため、1つのStoryboardに収めると管理しづらくなってきました。
Storyboard画面を頻繁に拡大縮小するのはかなり煩わしいです (私はノートパソコンなので特に、、、)。
個人開発でさえこのように感じるのであれば、チーム開発では尚更であることは想像に容易いです。
本記事では、Storyboard Referenceを使用して、Storyboardを分割する手順をまとめてみました。環境
[Xcode] Version 12.4
[Swift] Version 5.3.2
[iOS] 14.4
[MacOS] 10.15.7今回作成するサンプル
下図のようなシンプルな画面遷移アプリを想定します。
各タブごとにStoryboardを (赤、青、緑に)分割し、最終的には同じ画面(紫)に遷移できるようにします。
Storyboard Referenceを作成する
Storyboardを分割する際、以下の2パターンの状況が想定できます。
1. すでに画面遷移が完成しており、Storyboardを切り分ける
2. 最初からStoryboardを分けて作成する
本記事では、1
の方法を行います。
最後に紫画面への参照を作るときのみ2
でやってみます。まず、切り分けたい画面を複数選択し、[
Editor
] > [Refactor to Storyboard...
]をクリックします。
切り分けるStoryboard名 (今回はRed.storyboard)と、そのファイルを入れるフォルダ (Group)を選択して[Save
]をクリックします。
「Storyboard
」フォルダなんかを作ると良いかもしれません。ファイルを選択して右クリック
> [New Group from Selection
]でフォルダを作成できます。
他の画面 (青、緑)も同様に切り分けると、メインのStoryboardは以下のようになります。
切り分けたStoryboardを見てみると、紫画面へのSegueが切れています。これは後で繋ぎ直す必要があります。
また、Storyboardの最初の画面には、「Attributes inspector
」の「is initial View Controller
」にチェックを入れておく必要があります。
以上で完了です。紫画面のStoryboardを
2
の方法で分割して画面遷移できるようにします。
まず、新しいstoryboardファイルを作成します (ファイル名はPurple.storyboardにしておきます)。
ViewControllerを作成します。
あとは遷移元のStoryboardでStoryboard Reference
を作成するだけです。
[Attributes inspector
]の「Storyboard
」に紐付けたいStoryboard名を入力します。
今回は画面遷移させたいだけので、Storyboard ReferenceにSegueを繋いで完了です。
[補足] TabBarのアイコンやタイトルを変更する
Storyboard Referenceを用いてStoryboardを分割したとき、TabBarのアイコン等をどこで変更するのか少し迷ったので補足します。
初めは、TabBarControllerの方で設定すると思っていたのですが、titleやimageを変更してアプリを立ち上げても反映されませんでした。
参照先のStoryboardの親ViewControllerで設定すると反映されるようです。
正しく反映されていました。
最後に
今回、初めてStoryboardを分割してみましたが、非常に簡単であり、Storyboardも管理しやすくなりました。
個人的には、まず1つのStoryboardで画面遷移を作成し、最後に切り分けていく1の方法が効率的と感じました。
より良い方法があれば教えていただけると幸いです。参考文献
以下の情報を参考にさせていただきました。
- Storyboardをあとから簡単に分ける方法
- 投稿日:2021-03-05T15:14:40+09:00
【Swift】NavigationBarのtitleを『文字列 + 画像』にする
どういうことか
こういうことがやりたい
文字を書くだけであればControllerに
title = "タイトル部分です"
と書くだけでいい。
実装する
extensionを使う。
Controllerの一番下にでも書こう。ここでは文字を 太字 にする設定も書いている。
細かい設定はともかくコピペで書いてもイケる。ViewController.swift// 文字列とアイコン画像を並べたタイトルを作る extension HomeViewController { func setTitle(_ title: String, andImage image: UIImage) { let titleLabel = UILabel() titleLabel.text = title titleLabel.font = UIFont.boldSystemFont(ofSize: 14) let imageView = UIImageView(image: image) let titleView = UIStackView(arrangedSubviews: [imageView, titleLabel]) titleView.axis = .horizontal titleView.spacing = 10.0 navigationItem.titleView = titleView } }そしたら
viewDidLoad()
内でこのメソッドに必要な値を渡す。ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // タイトルの文字列と画像の名前を渡す setTitle("タイトル部分です", andImage: UIImage(named: "user_icon")!) // 中略以上です(´・ω・`)
- 投稿日:2021-03-05T12:57:47+09:00
【Swift】シングルトンを1分で理解する
シングルトンとは
初期化処理を内部に梱包することで、外部から初期化しないようにし、
データを保持することのできる記法。
タスクキルしない限りデータを保持し続ける。
破棄(再初期化)されないので画面遷移してもデータが保持される。
言い換えればタスクキルでデータは消えるので
ローカルにデータを保存したい場合は、使えない。class Singleton { var name:String = "" //内部に梱包>> static let shared = Singleton() private init() { //何も初期化しない } //<< }使い方
print(Singleton.shared.name) //"" Singleton.shared.name = "hoge" print(Singleton.shared.name) //"hoge" //...画面遷移 print(Singleton.shared.name) //"hoge"
- 投稿日:2021-03-05T11:22:29+09:00
【Swift】RxDataSource(MVVM)の実装手順
1.Entityを用意する
struct Data { var id var name }もちろんここはAPIの受け口であるCodableなEntityでも良いですし、
そういう使い方の方が多いかもしれません。2.DataSourceを実装
import RxDataSources struct DataSource { var items: [Data] } extension DataSource: SectionModelType { init(original: DataSource, items: [Data]) { self = original self.items = items } }セクションモデルなので、セクションごとのモデル定義が前提だが、
もちろんセクションが1つの場合でもok3. register
tableView.register(UINib(nibName: identifier, bundle: nil), forCellReuseIdentifier: identifier)collectionviewの場合はここをcollectionviewで
4. Cellのsetup関数自作(ここは自由)
//cellファイル func setupCell(item: Data) { hogeId = item.id hogeName = item.name }5. datasource (6が先でも良い)
//grobal変数>> //CollectionViewならRxCollectionViewSectionedReloadDataSource var datasource: RxTableViewSectionedReloadDataSource<[DataSource]>? //<< ... //CollectionViewならRxCollectionViewSectionedReloadDataSource datasource = RxTableViewSectionedReloadDataSource<DataSource>(configureCell: { _, tableView, indexPath, items in let cell = TableViewUtil.createCell(tableView, identifier: cellのidentifier, indexPath) as! hogehogeTableViewCell //cellのセットアップ関数を作りそこにdatasourceを流す //itemsのtypeはData cell.setupCell(item: items) return cell })6. setDelegate
//grobal変数>> private var disposeBag = DisposeBag() //<< //普通のTableViewDelegateが使える tableView.rx.setDelegate(self).disposed(by: disposeBag) //セル選択時にindexPathが送られてくるのでViewModelに流す tableView.rx.itemSelected .subscribe(onNext: { [weak self] indexPath in guard let self = self else {return} hogeViewModel.input.tableviewSelected.onNext(indexPath) }).disposed(by: disposeBag)7.ViewModel(KickstarterのVMインターフェース)
protocol ViewModelInput { var tableviewSelected: AnyObserver<Void> {get} var fetchData: AnyObserver<Void> {get} } protocol ViewModelOutput { var dataSource: Observable<[DataSource]> {get} } protocol ViewModelType { var inputs: ViewModelInput {get} var outputs: ViewModelOutput {get} } class ViewModel: ViewModelInput, ViewModelOutput { //input var tableviewSelected: AnyObserver<Void> var fetchData: AnyObserver<Void> //output var dataSource: Observable<[DataSource]> private var localDataSource:[DataSource]? private var disposeBag = DisposeBag() //VCヘの遷移でViewModelのインスタンスを作るときに //データを渡してあげたい場合はinitに投げる init(dataSource: [DataSource]) { localDataSource = dataSource let _tableviewSelected = PublishRelay<IndexPath>() tableviewSelected = AnyObserver<IndexPath>() { indexPath in guard let indexPath = indexPath.element else {return} _tableviewSelected.accept(indexPath) } let _fetchData = PublishRelay<Void>() fetchData = AnyObserver<Void>() { indexPath in guard let indexPath = indexPath.element else {return} _fetchData.accept(Void()) } let _dataSource = PublishRelay<[DataSource]>() dataSource = _dataSource.asObservable() _tableviewSelected.subscribe({ [weak self] indexPath in guard let self = self else {return} guard let indexPath = indexPath.element else {return} //indexPathがくるのでdatasourceをいじり //_dataSource.acceptでデータを渡してやる }).disposed(by: disposeBag) _fetchData.subscrive({ [weak self] _ in //ここでAPIを投げてレスポンスを_dataSourceにacceptする }).disposed(by: disposeBag) //ローカルにデータがあるなら普通に_dataSource.accept([DataSource])でok } }8.outputのdatasourceをtableviewにbind
//これでviewModelからのdatasourceがtableviewにバインドされ、 //値の変更がリアルタイムで反映される viewModel.outputs.dataSource.bind(to: tableView.rx.items(dataSource: datasource!)) .disposed(by: disposeBag)9.まとめ
ViewModelからdatasourceをViewControllerのtableViewにバインドできていればok
データをいじるときはviewModelのオブザーバに購読させ、
そこでデータを加工し、加工したデータをdatasourceにacceptすれば
tableviewにバインドされているのでデータが反映される流れ。fetchDataにイベントを送る処理やAPI部分は
書いていません。(API処理もRxSwiftがわかる前提で書いています)
- 投稿日:2021-03-05T11:12:00+09:00
presentとかpopとかpushの違い
超ざっくりまとめます
present
ビューの上にもう一つビューが乗るイメージ
pop
次の画面に遷移
push
複数回画面遷移した後に、いくつか前の画面に一気に戻りたい場面で使う
時間があるときに更新します。
- 投稿日:2021-03-05T10:59:41+09:00
モーダルビューをフルスクリーンで表示
let vc = UIViewController()
// 遷移方法にフルスクリーンを指定
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)これだけです。
- 投稿日:2021-03-05T06:38:36+09:00
【2回目】初心者が自作iOSアプリ審査に挑戦中
なぜ書くのか
つまずいたポイントを記録するため。
僕と同じように、初めてApp Storeに自作アプリを公開しようとしている人の参考にしてもらうため。以下の記事の続きです。
【いったん頓挫】初心者がApp StoreにiOS自作アプリを公開する過程iOSアプリ審査の指摘事項
今回は審査の請求から20時間ほどで返信が返ってきました。
Guideline 2.3.8 - Performance - Accurate Metadata
以下本文
We noticed that the app name to be displayed on the App Store and the app name displayed on the device do not sufficiently match, which makes it difficult for users to find the app they just downloaded. The app names are:
Name on the App Store: AtaCon
Name displayed on the device: JandTlanguage
App Storeで表示される名前とダウンロードした後にホーム画面に表示されるアプリアイコンの下の文字が全然違いますよ。利用者が混乱するよ、という指摘でした。
アプリアイコンの下の文字を「AtaCon」になるように修正しました。Guideline 4.2 - Design - Minimum Functionality
We found that the usefulness of your app is limited by the minimal amount of content or features it includes.
Next Steps
We encourage you to review your app concept and incorporate different content and features that are in compliance with the App Store Review Guidelines.
We understand that there are no hard and fast rules to define useful or entertaining, but Apple and Apple customers expect apps to provide a really great user experience. Apps should provide valuable utility or entertainment, draw people in by offering compelling capabilities or content, or enable people to do something they couldn't do before or in a way they couldn't do it before.
また出ました。リジェクト理由が書かれているページの下にこんな欄があるのを発見したので一回クレームを入れてみようかと思います。
以下送信内容(ほぼGoogle翻訳です)
Thank you for your review.
I would like to write about the features of this app.There is a fact that when learning a language it is good to imitate the pronunciation of native speakers.
In addition, there is a learning method called instant English composition in English.
This is a learning method that repeats training to translate Japanese sentences into English the moment you see them.
This learning method is quite famous in Japan.
Also released in your store.
I tried to use that learning method to study Mandarin Chinese, but I couldn't find anything remarkable in mobile app that could put that learning method into practice.
So I made this app.
The UI design is fairly simple, but incorporates native speaker voice and scholarly techniques such as instant English composition.Is the amount of example sentences insufficient?
I plan to add example sentences on a regular basis after the release.Please check it.
気づいたこと
前回指摘された「Guideline 1.5 - Safety - Developer Information」は前回修正した内容でいったん通ったのかなと思います。(Tweetに連絡先を書いて、そのTweetのURLを貼る)
まとめ
「Guideline 4.2 - Design - Minimum Functionality」に負けず、リリースまで漕ぎ着けたいと思います!