20210305のSwiftに関する記事は16件です。

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

【Swift】MVVMとは??でモヤモヤしてる人向けの図解付き解説

前提

ある程度調べていてアーキテクチャという言葉の意味くらいならわかる
RxSwiftを自分なりに調べていて読めはする

MVVM

まず、アプリの使われ方、あり方について考えてみる。
アプリを使うのはユーザーで、ユーザーはアプリを開き、
つまり画面を開き、操作を行う。
この操作に対して、ViewModelやModelのロジックが働き、
Viewはそれらのロジックの結果を画面に反映する。

このように、何かのイベントに対して何かのロジックが働き、結果を反映する。
その流れの中で、表示非表示や変形、バリデーション、イベントに対するロジックなど
役割ごとに分け、責務の切り分けをするのがアーキテクチャであり、MVVMである。

図解

スクリーンショット 2021-03-05 22.23.53.png

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がわかったので備忘録として。
間違っている点があればコメントしていただけると幸いです。

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

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
    }

これで完了です!
あとは開いて動作を確認してみてください。

Videotogif.gif

まとめ

いかがでしたでしょうか。とっても簡単ですね。

これは非常に便利なので、使わない理由がありません!

是非是非ご活用ください〜(それにしても先人は偉大ですね…)

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

filterの使い方について簡単にまとめる

filterは配列のデータでいらないものを除去したりする際に使います。

使い方としてはこんな感じ

配列を取得

配列をfilterする

プログラムだとこんな感じ

let data: [テーブル名] = テーブルを取得する関数

data.filter({$0.レコード名 == 値})

$0 ← これはなんだと思う人が多いと思うんですが、

簡単に言うとテーブル名です。

なのでdata.filter({$0.レコード名 == 値})は日本語で伝えると

データ.フィルター({テーブル名のレコード = 値})

ループで回してifで値を追加していくみたいな処理も全然ありなんですけど、結構ソースも見づらいしループだらけで結構処理も重くなってしまうんですよね。

なんでfilterを使いこなせるようになると割と便利かもです。

とはいえ、いきなり初学者が覚えられるかと言われると割と難しいので、実践などを通してあーこういうことね

なんて思えるようになればいいかなと思います。

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

【Swift】アプリを再インストールした際にFCMのリモート通知が届かなくなった

環境

  • Xcode Version 12.3 (12C33)
  • iOS 13 以上
  • Firebase/Messaging (7.1.0)

初期実装

AppDelegate では以下のような初期化処理を行っていた

AppDelegate.swift
import 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.swift
    private 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: アプリ再インストール時の挙動

MessagingDelegatefunc messaging(_ messaging:didReceiveRegistrationToken fcmToken:) を以下の通り実装

AppDelegate.swift
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {

        print("*** fcmToken: \(fcmToken ?? "")")
        print("*** fcmToken: \(Messaging.messaging().fcmToken ?? "")")
    }
  • didReceiveRegistrationTokenfcmTokenMessaging.messaging().fcmTokenが一致することは確認している
  1. 通知が届く状態のアプリをデバッグ実行しdidReceiveRegistrationToken デリゲートメソッドに届いたFCMトークンを確認
  2. FCMトークンが有効か curl コマンドで確認
  3. デバッグ実行を中止しアプリをアンインストール
  4. アプリをデバッグ実行(再インストール)しdidReceiveRegistrationToken デリゲートメソッドに届いたFCMトークンを確認
  5. FCMトークンが有効か確認
  6. 1分程度放置しdidReceiveRegistrationTokenデリゲートメソッドが再び発火するか確認
  7. PUSH通知が届くか確認
  8. アプリを再起動(デバッグ実行)
  9. didReceiveRegistrationToken デリゲートメソッドに届いたFCMトークンを確認
  10. FCMトークンが有効か確認
  11. 1分程度放置しdidReceiveRegistrationTokenデリゲートメソッドが再び発火するか確認
  12. PUSH通知が届くか確認

結果

  1. トークンAを得る
  2. トークンAは curl 有効であることがわかる
  3. 実行可
  4. トークンAを得る
  5. トークンAは curl 無効であることがわかる( "error":"No information found about this instance id."
  6. 発火しない(トークンAは更新されない)
  7. 届かない
  8. 実行可
  9. トークンBを得る
  10. トークンBは curl コマンドより有効であることがわかる
  11. 発火しない(トークンBは更新されない)
  12. 届く

検証2: トークン取得メソッドを変更しエラーハンドリングを試みる

MessagingDelegatefunc messaging(_ messaging:didReceiveRegistrationToken fcmToken:) を以下の通り実装

AppDelegate.swift
    func 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")")
        }
    }

以下の手順を試みる

  1. 通知が届く状態のアプリをデバッグ実行しdidReceiveRegistrationToken ログを確認
  2. FCMトークンが有効か curl コマンドで確認する
  3. デバッグ実行を中止しアプリをアンインストール
  4. アプリを再インストール(デバッグ実行)しdidReceiveRegistrationToken 内部のログを確認

結果

  1. 以下の通りトークンAを得る
*** fcmToken: [トークンA]
*** fcmToken: [トークンA]
*** Messaging.messaging().token
*** fcmToken: [トークンA]
*** fcmTokenError: nothing
  1. トークンA はcurl コマンドより有効であることがわかる
  2. 実行可能
  3. 以下のログを得る
*** fcmToken: [トークンA]
*** fcmToken: [トークンA]
*** fcmToken: [トークンB]
*** fcmToken: [トークンB]
*** Messaging.messaging().token
*** fcmToken: [トークンB]
*** fcmTokenError: nothing
*** Messaging.messaging().token
*** fcmToken: [トークンB]
*** fcmTokenError: nothing

curl コマンドによる追加検証で以下が判明している

  • 上記ログ2行目時点ではトークンAは有効
  • 上記ログ3行目時点ではトークンAは無効
  • トークンBは有効

検証結果まとめ

  1. アプリを再インストールした際にdidReceiveRegistrationTokenでアンインストール前に利用していたトークンが返却される
  2. Messaging.messaging().fcmToken では didReveiveRegistrationToken で得られるトークンと同じものを取得する
  3. didReceiveRegistrationToken発火後何らかの原因でアンインストール前に利用していたトークンが無効化される
  4. 無効化された後アプリを再起動することで正常に通信可能なトークンをdidReceiveRegistrationTokenMessaging.messaging().fcmTokenで利用可能となる
  5. Messaging.messaging().token(completion:)メソッドを用いることで再インストール後にも有効なFCMトークンを得ることができる
  6. その際Messaging.messaging().token(completion:)メソッドにはエラーが入ってこない
  7. Messaging.messaging().token(completion:)メソッドを呼び出すとdidReveiveRegistrationTokenが発火し、内部で有効なFCMトークンを得ることができる
  8. Messaging.messaging().token(completion:)メソッドを呼び出した後にはMessaging.messaging().fcmTokenプロパティで有効なFCMトークンを得ることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftのメモ

textField.resignFirstResponder()は、キーボーどを呼び出しした際に、returnキーで消えてくれるもの。もしくは、キーボード以外の画面をタッチすると、消えてくれる

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

Swiftのメモ(素人が趣味でやってるのをメモしてるやつ。自分用)

:relaxed:
textField.resignFirstResponder()は、キーボーどを呼び出しした際に、returnキーで消えてくれるもの。もしくは、キーボード以外の画面をタッチすると、消えてくれる

:sunglasses:
dismiss(animated: true, completion: nil)で、画面遷移する前の画面に戻れる感じ?

:frowning2:
performSegue(withIdentifier: "SegueにつけるID名", sender: nil)
これで指定したSegueから移行させることができるはず・・・できた一応

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

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.4

Swiftの開発は構造体(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 = "吉田沙保里"

出力結果

吉田沙保里さんが飼い主になりました。

プロトコルでストアドプロパティやプロパティオブザーバは定義できない仕様になっているので、参照先の構造体で個別に定義してあげないといけない。また、プロパティオブザーバを実行させるためにインスタンス化時にいったん別の値を入れないといけない。
全然スマートじゃないので、参照元でストアドプロパティやプロパティオブザーバを定義したい場合はクラス型が良い。

まとめ

クラスを採用したほうが良いと判断する基準は、

  1. 参照先で更新されたプロパティをインスタンスにも共有させたいか
  2. デイニシャライザを使いたいか
  3. 参照元でプロパティを定義したいか。それを複数の参照先で共有したいか

になるのかなぁ、と書籍を読んで思いました。

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

【Swift】Compositional Layoutsで実現する疎結合な実装

はじめに

TOP IMG

Compositional LayoutsがWWDC2019で発表され、ここ数ヶ月でようやくiOS13以上をターゲットにしたプロジェクトが増えてきたのではないでしょうか?

SwiftUIを取り入れている技術の記事も目立ってきましたが、iOS14にならないと不自由も多く、最初から機能が豊富なCompositional Layoutsを選択するのも1つの判断かと思います。本記事では実際にプロジェクトに導入してみたので、どのような構成で導入してみたのかをまとめています。

Compositional Layouts の優位性

そもそも、Compositional Layoutsで組むことは、何がメリットなのかというお話をざっくりしておきます。

1. UICollectionViewDelegateFlowLayout のデメリット

iOS12 以下でUICollectionViewを用いて複雑なレイアウトを組む場合、こちらを検討する人が多いでしょう。

UICollectionViewDelegateFlowLayoutViewControllerに継承して、画面の設定を直接書いていきます。

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密結合してしまうことです。どうして密結合になってしまうのかというと、それは継承の関連に問題があります。

Screen Shot 2021-03-01 at 12.01.22.png

UICollectionViewの実装ではUICollectionViewDelegateの実装がほとんどの場合で必要になります。UICollectionViewDelegateFlowLayoutはそれを継承しているため、ViewControllerから実装が剥がせないのです。※1

これはViewControllerの肥大化にも繋がりよくありません。仮にenumstructで設定を定数化して切り出したり、分岐処理を切り出すことはできても、呼び出し部分はどうしても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 ArchitectureVIPERなど、疎結合が実現可能なアーキテクチャーであれば、同じような形で実装を行うことができるでしょう。

以下は、実際に運用しているアプリの構成を簡単にまとめたものです。
arc

先に結論の概要から述べてしまうと、上の構成にCompositional Layoutsを導入するとこのようになります。

Screen Shot 2021-03-05 at 11.06.14.png

抽象的なプロトコルとして書き出すことで共通化し、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番号で分岐できるため、この部分を抽象化して取り出すことで、すっきりとした書き方にすることができます。

具体的には、以下のレイアウトがあった場合、図右側のような抽象化を行います。
layout

セクションに共通する処理を整理して抽象化していきます。
例として、抽象化するとこのようになります。

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構成にするだけなので、構成の仕方に関して記載は致しません。後述のリポジトリを見ていただけると幸いです。

構成の例として、階層構造を置いておきます。
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)
    }
}

先ほどの図に照らし合わせると、

Screen Shot 2021-03-05 at 16.01.27.png

このような関係になっています。

このように、抽象化して分離することで、ViewControllerの肥大化を防ぎつつ、疎結合な作りを実現することができるのです。

終わりに

少しコードが多くなってしまい、わかりづらい部分もあるかもしれません。

動作するリポジトリを置いておくので、こちらからコードを読んでいただけると幸いです。
- CompositionalLayouts-MVP

また、この実装はCompositional LayoutsをiOS12以下で使用するためのバックポートライブラリである
- kishikawakatsumi / IBPCollectionViewCompositionalLayout

をベースにしています。

そちらをみていただくと、より理解を深めることができます。
ご指摘などありましたらコメントいただけると幸いですmm

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

[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を (赤、青、緑に)分割し、最終的には同じ画面(紫)に遷移できるようにします。
1.png

Storyboard Referenceを作成する

Storyboardを分割する際、以下の2パターンの状況が想定できます。
1. すでに画面遷移が完成しており、Storyboardを切り分ける
2. 最初からStoryboardを分けて作成する
本記事では、1の方法を行います。
最後に紫画面への参照を作るときのみ2でやってみます。

まず、切り分けたい画面を複数選択し、[Editor] > [Refactor to Storyboard...]をクリックします。
2.png
切り分けるStoryboard名 (今回はRed.storyboard)と、そのファイルを入れるフォルダ (Group)を選択して[Save]をクリックします。
3.png
Storyboard」フォルダなんかを作ると良いかもしれません。ファイルを選択して右クリック > [New Group from Selection]でフォルダを作成できます。
4.png
他の画面 (青、緑)も同様に切り分けると、メインのStoryboardは以下のようになります。
5.png
切り分けたStoryboardを見てみると、紫画面へのSegueが切れています。これは後で繋ぎ直す必要があります。
また、Storyboardの最初の画面には、「Attributes inspector」の「is initial View Controller」にチェックを入れておく必要があります。
6.png
7.png
以上で完了です。

紫画面のStoryboardを2の方法で分割して画面遷移できるようにします。
まず、新しいstoryboardファイルを作成します (ファイル名はPurple.storyboardにしておきます)。
8.png 9.png
ViewControllerを作成します。
10.png
あとは遷移元のStoryboardでStoryboard Referenceを作成するだけです。
11.png
[Attributes inspector]の「Storyboard」に紐付けたいStoryboard名を入力します。
12.png
今回は画面遷移させたいだけので、Storyboard ReferenceにSegueを繋いで完了です。
13.png

[補足] TabBarのアイコンやタイトルを変更する

Storyboard Referenceを用いてStoryboardを分割したとき、TabBarのアイコン等をどこで変更するのか少し迷ったので補足します。
初めは、TabBarControllerの方で設定すると思っていたのですが、titleやimageを変更してアプリを立ち上げても反映されませんでした。
14.png
参照先のStoryboardの親ViewControllerで設定すると反映されるようです。
15.png
正しく反映されていました。
16.png

最後に

今回、初めてStoryboardを分割してみましたが、非常に簡単であり、Storyboardも管理しやすくなりました。
個人的には、まず1つのStoryboardで画面遷移を作成し、最後に切り分けていく1の方法が効率的と感じました。
より良い方法があれば教えていただけると幸いです。

参考文献

以下の情報を参考にさせていただきました。
- Storyboardをあとから簡単に分ける方法

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

【Swift】NavigationBarのtitleを『文字列 + 画像』にする

どういうことか

こういうことがやりたい

Simulator Screen Shot - iPhone 11 - 2021-03-05 at 15.02.26.png

文字を書くだけであれば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.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        // タイトルの文字列と画像の名前を渡す
        setTitle("タイトル部分です", andImage: UIImage(named: "user_icon")!)

        // 中略

以上です(´・ω・`)

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

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

【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つの場合でもok

3. 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がわかる前提で書いています)

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

presentとかpopとかpushの違い

超ざっくりまとめます

present

ビューの上にもう一つビューが乗るイメージ

pop

次の画面に遷移

push

複数回画面遷移した後に、いくつか前の画面に一気に戻りたい場面で使う

時間があるときに更新します。

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

モーダルビューをフルスクリーンで表示

let vc = UIViewController()
// 遷移方法にフルスクリーンを指定
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)

これだけです。

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

【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.

また出ました。リジェクト理由が書かれているページの下にこんな欄があるのを発見したので一回クレームを入れてみようかと思います。

スクリーンショット 2021-03-05 6.27.31.png

以下送信内容(ほぼ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」に負けず、リリースまで漕ぎ着けたいと思います!

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