20200204のSwiftに関する記事は13件です。

buttonを丸角に!

SwiftにおけるButtonの装飾についての備忘録

Swiftにおいてcss的な役割をするものは何だろうと思い、調べた結果を以下に記します。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var nextButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        nextButton.layer.cornerRadius = 20 //ここで丸角を指定
    }
}

・buttonの名前をnextButtonとしました。

結果

スクリーンショット 2020-02-04 23.45.39.png

以上になります、ご参考までに。。。

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

SwiftUIとRealmの連携方法

はじめに

SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。

要点

  • Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
  • Realmオブジェクトを @Published で保持したとしても、値に変更があった際に自動で更新されるようにはならない
  • observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる

Realmのインストール

RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします

  1. Dependencyを追加する
    image

  2. realm-cocoa を選択する
    image

  3. バージョンを指定する
    image

  4. プロダクトを選択する(両方選択します)
    image

  5. import RealmSwift を記述すれば利用可能になる

Entityの用意

  • まずは準備としてRealmで保持するデータの型を定義します。
  • 今回は動作検証用として、以下の挙動を行うようにしています
    • setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift

class ItemEntity: Object, Identifiable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""

    override class func primaryKey() -> String? { "id" }
    override class func indexedProperties() -> [String] { ["id"] }

    private static var realm = try! Realm()

    static func setUp() {
        Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in
            try! realm.write {
                realm.deleteAll()
                realm.add(createFixture(), update: .modified)
            }
        }
    }

    static func all() -> Results<ItemEntity> {
        realm.objects(ItemEntity.self)
    }

    private static func createFixture() -> [ItemEntity] {
        (0..<10)
            .map { _ in (0..<1000).randomElement()! }
            .map { number -> ItemEntity in
                let item = ItemEntity()
                item.id = "\(number)"
                item.name = "item\(number)"
                return item
            }
    }
}

ObservableObjectを利用する

  • Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
  • しかし、このコードは最初にフェッチしたデータしか表示されません。
    • @Published で変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない
            ForEach(viewModel.itemEntities) { itemEntity in
                Text(itemEntity.name)
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
}

image

対応方法

  • それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
  • Realmのオブジェクトが持つ observe メソッドを利用することでこれを実現することができます
  • このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in
    switch change {
    case let .initial(results):
        // do something
    case let .update(results, deletions, insertions, modifications):
        // do something
    case let .error(error):
        // do something
    }
}
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in
                if itemEntity.isInvalidated {
                    EmptyView()
                } else {
                    Text(itemEntity.name)
                }
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す
        notificationTokens.append(itemEntities.observe { change in
            switch change {
            case let .initial(results):
                self.itemEntities = results
            case let .update(results, _, _, _):
                self.itemEntities = results
            case let .error(error):
                print(error.localizedDescription)
            }
        })
    }

    deinit {
        notificationTokens.forEach { $0.invalidate() }
    }
}

※ もしくは、 @Published を利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。

import Combine
class Store: ObservableObject {
    var objectWillChange: ObservableObjectPublisher = .init()
    private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        notificationTokens.append(itemEntities.observe { _ in
            // SwiftUIに再レンダリングが必要なことを伝える
            self.objectWillChange.send()
        })
    }

    // ...
}

この実装によって、自動更新の挙動を実現することができました。(gif参照)
image

(おまけ)EnvironmentObjectを利用する

  • 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
  • この変更はとても簡単で以下の修正を入れるだけです
    • (ViewModelをStoreにリネーム)
    • Viewが保持するインスタンスを @ObservedObject から @EnvironmentObject に変更
    • Viewの初期化時に environmentObject Modifierでデータを保持するオブジェクトを受け渡す
  • これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject {
+ class Store: ObservableObject {
    // ...
}
struct ContentView: View {
    - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel
    + @EnvironmentObject private var store: Store
let contentView = ContentView()
+    .environmentObject(Store())

サンプルコード

https://github.com/chocoyama/SwiftUI-Realm

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

SwiftUIとRealmを連携してUIを自動更新する

はじめに

SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。

要点

  • Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
  • Realmオブジェクトを @Published で保持したとしても、値に変更があった際に自動で更新されるようにはならない
  • observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる

Realmのインストール

RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします

  1. Dependencyを追加する
    image

  2. realm-cocoa を選択する
    image

  3. バージョンを指定する
    image

  4. プロダクトを選択する(両方選択します)
    image

  5. import RealmSwift を記述すれば利用可能になる

Entityの用意

  • まずは準備としてRealmで保持するデータの型を定義します。
  • 今回は動作検証用として、以下の挙動を行うようにしています
    • setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift

class ItemEntity: Object, Identifiable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""

    override class func primaryKey() -> String? { "id" }
    override class func indexedProperties() -> [String] { ["id"] }

    private static var realm = try! Realm()

    static func setUp() {
        Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in
            try! realm.write {
                realm.deleteAll()
                realm.add(createFixture(), update: .modified)
            }
        }
    }

    static func all() -> Results<ItemEntity> {
        realm.objects(ItemEntity.self)
    }

    private static func createFixture() -> [ItemEntity] {
        (0..<10)
            .map { _ in (0..<1000).randomElement()! }
            .map { number -> ItemEntity in
                let item = ItemEntity()
                item.id = "\(number)"
                item.name = "item\(number)"
                return item
            }
    }
}

ObservableObjectを利用する

  • Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
  • しかし、このコードは最初にフェッチしたデータしか表示されません。
    • @Published で変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない
            ForEach(viewModel.itemEntities) { itemEntity in
                Text(itemEntity.name)
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
}

image

対応方法

  • それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
  • Realmのオブジェクトが持つ observe メソッドを利用することでこれを実現することができます
  • このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in
    switch change {
    case let .initial(results):
        // do something
    case let .update(results, deletions, insertions, modifications):
        // do something
    case let .error(error):
        // do something
    }
}
struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in
                if itemEntity.isInvalidated {
                    EmptyView()
                } else {
                    Text(itemEntity.name)
                }
            }
        }.onAppear {
            ItemEntity.setUp()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す
        notificationTokens.append(itemEntities.observe { change in
            switch change {
            case let .initial(results):
                self.itemEntities = results
            case let .update(results, _, _, _):
                self.itemEntities = results
            case let .error(error):
                print(error.localizedDescription)
            }
        })
    }

    deinit {
        notificationTokens.forEach { $0.invalidate() }
    }
}

※ もしくは、 @Published を利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。

import Combine
class Store: ObservableObject {
    var objectWillChange: ObservableObjectPublisher = .init()
    private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all()
    private var notificationTokens: [NotificationToken] = []

    init() {
        notificationTokens.append(itemEntities.observe { _ in
            // SwiftUIに再レンダリングが必要なことを伝える
            self.objectWillChange.send()
        })
    }

    // ...
}

この実装によって、自動更新の挙動を実現することができました。(gif参照)
image

(おまけ)EnvironmentObjectを利用する

  • 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
  • この変更はとても簡単で以下の修正を入れるだけです
    • (ViewModelをStoreにリネーム)
    • Viewが保持するインスタンスを @ObservedObject から @EnvironmentObject に変更
    • Viewの初期化時に environmentObject Modifierでデータを保持するオブジェクトを受け渡す
  • これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject {
+ class Store: ObservableObject {
    // ...
}
struct ContentView: View {
    - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel
    + @EnvironmentObject private var store: Store
let contentView = ContentView()
+    .environmentObject(Store())

サンプルコード

https://github.com/chocoyama/SwiftUI-Realm

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

依存度の低い実装〜中級エンジニアを目指して

何を書いた記事か

駆け出し・初級エンジニアから、中級エンジニアに成長するための一つの指標として、コードの保守性、特にライブラリ依存について書きました。
駆け出し・初級エンジニアは、動くコードが書けさえすればいいのですが、中級エンジニアから、保守性の観点でコードが書けるようになるべきです。
でほ保守性の高いコードとは何なのか、その具体例として今回はライブラリ依存について考えていきます。

前提・使用技術

Swiftで実装した簡単なiOSアプリで、バックエンドとしてFirebaseを利用しているケースを例にあげます。
 ※Swift/iOSの基本やFirebaseについては特に触れません。

雑に作ったアプリ

アプリの概要

ログイン・サインアップ・Twitterログイン・プロファイル登録・登録内容確認の機能しか持たない、シンプルなアプリを作りました。
ログインやプロファイル登録などの各工程でFirebaseと通信を行なっています。
image.png

アプリの実装

Udemyなどでよく見る、下記の方法で実装しています。

  • ソースはMVCライクに構造化されてはいる
  • 一方、各ViewControllerに画面内での処理(Firebase通信含む)を全て記載している

ソースは、Model / View / Controllerごとにグルーピングして、storyboardはView層、ViewControllerはController層に配置しています(Modelはこの段階ではありません)

一つのViewController、例えばLoginViewControllerを見ると、下記のように、Firebaseのライブラリを直接importして、Viewで実施したい処理を全て記載しています。

LoginViewController.swift(抜粋)
import UIKit
import Firebase

class LoginViewController: UIViewController {

    @IBOutlet weak var userIdField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "loginSegue" {
            guard let userId = userIdField.text else {
                return
            }
            let resultVC: AuthResultViewController = segue.destination as! AuthResultViewController
            resultVC.userId = userId
        }
    }

    @IBAction func login(_ sender: Any) {
        guard let userId = userIdField.text else {
            return
        }
        guard let password = passwordField.text else {
            return
        }
        Auth.auth().signIn(withEmail: userId, password: password) { (result, error) in
            guard let result = result else {return}
            print(result)
            self.performSegue(withIdentifier: "loginSegue", sender: userId)
        }
    }
}

他のViewControllerでも同じように、Firebaseでの処理などその画面で行いたい操作を全て記載しています。

何が問題なのか?

出来上がったソース(ViewController)をみてみましょう。Firebase関連の処理部分を赤枠で囲ってあります。
image.png
image.png

ではここで、バックエンドで利用しているFirebaseに仕様変更が入った、あるいはAmplifyや自前APIサーバなどに移行するなどの要件が発生して、コードのFirebase部分を変更しなければならなくなった状況をイメージしてみてください。

たった5画面のアプリなのに、修正加える必要がある箇所が散らばっていて、萎えませんか?
さらには変えたことによってViewの挙動が変わるリスクがあり、おいそれと手を入れたくない気持ちが勝手しまいそうです。

こういったコードは、動くことを最優先にして構成を考えずに書いてしまうと気づいたら出来上がってしまっているケースが多いです。
特に初学者の方の個人開発アプリなどで見かけます。
(世の中の教材の大半がこういう実装になってるので仕方ないかと。。)

リファクタリング

リファクタの方針

では修正に耐えられるようなコードに変えていきましょう。
やることはシンプルで、「Firebase通信を行なっている箇所を一箇所にまとめ、VCとの依存を断ち切る」です。

修正したコード

コードの構成は下記のようになります。Model層にファイルが増えていることがわかるかと思います。

image.png

まずはAPI通信系の処理で実装しなければいけないメソッドを集めたprotocolを作成します。

APIClientProtocol.swift
import Foundation

protocol APIClientProtocol {
    func signin(email: String, password: String,  completion: @escaping () -> Void)
    func createUser(email: String, password: String,  completion: @escaping () -> Void)
    func twitterLogin(comlpetion: @escaping () -> Void)
    func insertDB<T: Codable> (data: T, database: String, completion: @escaping () -> Void) -> String
    func selectDB(id: String, database: String, completion: @escaping ([String : Any]) -> Void)
    func timestampToString(date: NSObject) -> String
}

今回は実装しませんが、テストコード書くためにAPI処理をMock化してコンストラクタインジェクションするときなど、このProtocolは役に立つので、一旦Protocolを挟んで抽象化することには慣れておくといいと思います。

次に実際のFirebase関連処理を、上のProtocolを実装する形で定義します。

FirebaseClient.swift(抜粋)
import Foundation
import Firebase
import FirebaseFirestoreSwift

struct APIClient: APIClientProtocol {

    func signin(email: String, password: String, completion: @escaping () -> Void) {
        Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
            if error != nil {
                return
            }
            guard let result = result else {return}
            print(result)
            completion()
        }
    }

    func createUser(email: String, password: String, completion: @escaping () -> Void) {
        Auth.auth().createUser(withEmail: email, password: password) { (result, error) in
            if error != nil {
                return
            }
            guard let result = result else {return}
            print(result)
            completion()
        }
    }
// 中略
}

signIn、signUpのところだけ抜粋しました。
引数としてフォームから受け取った値をとって、処理が完了したときの操作をクロージャで実行しています。

次に、最初に見たLoginViewControllerがどう変わるか見てみましょう。

LoginViewController.swift
import UIKit

class LoginViewController: UIViewController {

    @IBOutlet weak var userIdField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "loginSegue" {
            guard let userId = userIdField.text else {
                return
            }
            let resultVC: AuthResultViewController = segue.destination as! AuthResultViewController
            resultVC.userId = userId
        }
    }

    @IBAction func login(_ sender: Any) {
        guard let userId = userIdField.text else {
            return
        }
        guard let password = passwordField.text else {
            return
        }
        let apiClient = APIClient()
        apiClient.signin(email: userId, password: password, completion: toNextVC)
    }

    private func toNextVC() -> Void {
        performSegue(withIdentifier: "loginSegue", sender: String.self)
    }

    @IBAction func twitterLogin(_ sender: Any) {
        let apiClient = APIClient()
        apiClient.twitterLogin(comlpetion: toNextVC)
    }

}

APIClientをインスタンス化して、ログインメソッドを実行しています。
Firebaseに関する処理が直接全く記載されていないところに注目してください。ViewControllerのレイヤーではFirebaseを意識しなくても済むような実装に変わっています。Firebaseライブラリもimportしていません。

ほかのViewControllerも同様なので、上で述べたようなバックエンドの変更が必要になった場合でも、修正するファイルは1つだけで、かつアプリのメインの挙動には手を入れなくてもいいような作りになっています。

まとめ

一応断っておくと、各ViewControllerにAPIロジックがベタで書かれているパターンが間違いという訳でもありません。
スピード重視でリリースするために、細かいことは後回しにしてまずは動くものを作る、という戦略を取ることも往々にしてあると思います。

アーキテクチャに正解はないのですが、常に「どうやったら保守性が高いコードが作れるか?」「どうやったら拡張しやすいか?変更に強いか?」などを考えて、その時その時で最適なアーキテクチャを考えることが、初級から中級への第一歩だよ、というのが僕が言いたかったことです。

まぁゆうて自分もSwift触り始めて1ヶ月とかなんで。一緒に勉強していきましょう。

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

シャッター音なしのアプリを作ってみた

作るに至ったわけ

日本や韓国などではスマホ(ケータイ含め)で写真を撮影しようとするとシャッター音が鳴りますが、海外で販売されたスマホ(iPhone含め)はシャッター音は鳴りません。インスタなどでレストランなどの静かなところで撮影する時など、あの音が鳴ると気を使いますよね。マナーからだという意見もありますが、スクリーンショットもなる必要はありません。法律で決まってるわけでないので、徐々にこの慣習がなくなっていくといいのになと思います。

というわけで、iOS向けにマナーモード時はシャッター音がならないカメラアプリを作ってみました。

無音カメラを作る方法

  • ビデオ撮影モードで静止画をキャプチャする方法

割とこの方法は一般的なようでぐぐると多く見つかります。ただ、静止画モードではないため解像度が劣ったりといろいろ制限があります。

  • 逆位相の音を再生する方法

ノイズキャンセリングと同じ方法ですが、ジャストのタイミングで再生しないといけないため、難易度は高いようです。

  • シャッター音のサウンドファイルを削除する方法

Jailbreakしかできません。
などなど。今回、どうやったかはまだ言えません。

審査を乗り越えるには

Appleの審査が通るまで約1ヶ月かかりました。基本的には、ユーザーに気付かないような録音、録画、撮影などは許可できないというもの。シャッター音を消すことはできませんと。特に、アプリ名からも連想させてはいけませんと。最初は「サイレントカム」っていう名前でした。

だって、AppStore見ると「無音カメラ」とかもろいってるアプリだってあるわけで、なんで???って感じでした。アプリの説明にも思いっきり書いてあるし、、神であるAppleが言うのだから仕方ありません。

未だ謎ですが、

  • アプリ名には「無音」「サイレント」と言った連想するキーワードは入れない
  • シャッター音をなるモードも用意する(今回のようにマナーモード以外はシャッター音鳴ります)

と言うのが今回の審査でわかりました。

使ったフレームワーク

  • AVFoundation.framework

ちょっと面倒でしたが、たまたまみたサンプルがこれだったのと、撮影画面とかをカスタマイズできたほうがいいかなと思ったのが理由です。

そしてリリース

C'zCam
https://apps.apple.com/jp/app/czcam/id585273753

静か目、静かカメラ、静かカメ、、、というわけで、C'zCamです。
よかったら試してみてください。無料です。

参考

https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/avcam_building_a_camera_app

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

SwiftをGoogle Colabで実装するための準備

SwiftをGoogle Colabで実装する手順を紹介します。

大まかなSTEPは 4つ です。

  1. Colabを作成し、Google Driveに保存する
  2. その他のアプリから、適当なエディタでファイルを開く
  3. "kernelspec":{"name":"python3","display_name":"Python 3"}"kernelspec":{"name":"swift","display_name":"Swift"} と書き換える
  4. Google Colabで開き、Swiftコードの動作確認をする

createColab.png

selectEditor.png

tapEditor.png

modifyCode.png

colab.png

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

NavigationBarのBackボタンの見た目を変えずにイベントをつける

はじめに

久々に投稿します!

NavigationBarの「<」Backボタンにイベントをつけたいと思い、調査をしましたが
見た目を変えずにイベントを付与することができる情報がなかなか見つからなかったので
備忘としてまとめてみます:point_up:

:warning::warning:iOS12以上をサポートしている前提で調査をしたため、SwiftUIは使用していません。:warning::warning:

開発環境

以下の環境で開発しました。

Tool Version
Xcode 11.2.1 (11B500)

やりたいこと

今回実現したいことは以下の通りです。

  1. NavigationBarのBackボタンタップ時にアラートを表示したい
  2. Backボタンの見た目は変えたくない
  3. Backボタンで戻る際のアニメーションも変えたくない

1番のみであれば、Backボタンを非表示にしてleftBarButtonItemに自作のBackボタンを追加すれば実現できそうですが、
2番3番の見た目を変えないというのが難しいです。

実現に向けたアプローチ

実現方法を2つ考えました。
1. NavigationBarのSubviewsを再帰的に取得して、どうにか「<」の画像を手に入れて自作のBackボタンに使用する
2. Backボタンのイベントを取得して、自作のイベントで上書きをする

1番のアプローチでは、画像取得はできましたが結局自分で画像やタイトルの位置を調整する必要が出てきました:no_good_tone1:
2番のアプローチで実現することができました。

具体的には、UIViewControllerにExtensionを実装します。
navigationController?.navigationBarのSubviewsを再帰的に探索してUIControlを取得し、
戻るイベントを削除し、新しいイベントを登録します。

実装

再帰的にSubViewsを取得する

以下を参考にさせていただきました。
Swiftで再帰的なサブビューの取得とUI層のユニットテストへの応用

UIView+RecursiveSubviews.swift
import UIKit

extension UIView {
    /// 再帰的にサブビューを取得する
    var recursiveSubviews: [UIView] {
        return subviews + subviews.flatMap { $0.recursiveSubviews }
    }

    /// UIViewの特定サブクラスのビューを取得する
    func findViews<T: UIView>(subclassOf: T.Type) -> [T] {
        return recursiveSubviews.compactMap { $0 as? T }
    }
}

UIControlのタップイベントをクロージャで登録する

ボタン等のイベントの登録addTarget(_:action:for:)をクロージャでするためのExtensionを作成しました。

UIControl+Tap.swift
import UIKit

typealias TapEvent = () -> Void

extension UIControl {

    /// タップイベントをクロージャで登録する
    func tap(action: @escaping TapEvent) {
        self.eventListener(controlEvents: .touchUpInside, forAction: action)
    }

    func eventListener(controlEvents control: UIControl.Event, forAction action: @escaping(() -> Void)) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

private extension UIControl {

    func actionHandler(action: (TapEvent)? = nil) {
        struct ActionHolder {
            static var action :(TapEvent)?
        }
        if let action = action {
            ActionHolder.action = action
        } else {
            ActionHolder.action?()
        }
    }

    @objc func triggerActionHandler() {
        self.actionHandler()
    }
}

NavigationBarのBackボタンにイベントを登録する

上記の2つのExtensionを使用して、
NavigationBarのBackボタンにイベントを登録するUIViewControllerのExtensionを作成しました。

UIViewController+AddNavigationBackEvent.swift
import UIKit

extension UIViewController {

    /// NavigationBarのBackボタンにイベントを登録する
    func addNavigationBackEvent(action: @escaping TapEvent) {

        guard let controls = navigationController?.navigationBar.findViews(subclassOf: UIControl.self) else {
            return
        }

        for control in controls {
            if control.allTargets.isEmpty {
                continue
            }
            control.removeTarget(nil, action: nil, for: .allEvents)
            control.tap(action: action)
            break
        }
    }
}

使い方

対象のViewControllerのviewDidLayoutSubviews()で呼び出します。
viewDidLoadで呼び出すと、Subviewsを取得できず、イベント登録ができません。

NavigationNextViewController
final class NavigationNextViewController: UIViewController {

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        self.addNavigationBackEvent { [weak self] in
            self?.confirmWhetherToBack()
        }
    }

    /// 前の画面に戻るかどうか確認するアラート表示
    private func confirmWhetherToBack() {
        let alert = UIAlertController(title: "確認", message: "入力内容が保存されていません。\n編集を終了しますか?", preferredStyle: .alert)

        // OKボタン
        alert.addAction(
            .init(title: "OK", style: .default, handler: { [weak self] _ in
                guard let `self` = self else {
                    return
                }
                self.navigationController?.popViewController(animated: true)
            })
        )

        // キャンセルボタン
        alert.addAction(
            .init(title: "Cancel", style: .cancel)
        )
        present(alert, animated: true)
    }
}

実行結果

イメージ通りの動きになりました:tada::tada:
demo.GIF

さいごに

やりたいことは実現できましたが、もう少し簡潔に実装できるのではないかと思っています:thinking:
もっと良い実装方法等ご存知でしたらご教示ください:innocent:

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

【swift】まず簡単なリファクタリングをしよう【初心者向け】

なぜ簡単なリファクタリングをするのか?

1.簡単なリファクタリングだけでも、コードの可読性が激増する。
2.難しい文法でリファクタリングをしようとすると、挫折する。
3.すぐ終わる。

大事なこと?

たまにリファクタリングをしたほうがいい。

リファクタリングで

  • 開発のしやすさ。
  • 説明のしやすさ。
  • コード修正のしやすさ。

を手に入れましょう✨

  • 説明のしやすさ。

これは、特に大事です。
誰かにアドバイスをもらうためコードを添付する際、
自分ですら読めないコードを誰が読めるのでしょう。

リファクタリングしないと??

初心者の方は初めてのアプリ(ポートフォリオなど)を作成する際、
『文法とかコードの可読性とかどうでもいいからとりあえず完成させたい!』という気持ちがあると思います。

しかし、コードが増えれば増えるほど

  • 以前書いたコードが理解できない
  • 変数名、メソッド名がパッと見わからない

などの問題が発生します。

簡単なリファクタリング集

わかりやすい命名をする

  • codicを使って命名する。
  • 命名規則を学んでみる→ 参考

三項演算子を使う

let hoge = true

if hoge {
    print("true")
} else {
    print("false")
}
// ↓三項演算子を使った表現。
hoge ? print("true") : print("false")

使用しないコードは削除する

コメントアウトして、放置しているコードはありがちですよね。
思い切って削除しましょう。

コメントは分かりやすく

パッと見で理解できるコメントを残しましょう。
参考

selfは省略可能

省略できる箇所は、するべきでしょう。
参考

参考文献

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

Selector完全攻略、そして初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について

前回の記事はNotificationCenterの基本でした。
その中で、難易度的に省いたトピックを今回書きます。

偉く長いタイトルにしましたが、トピックとしては二点で、

  • Selectorの話
  • AddTarget()/AddObserver()に変数渡したいときどうするか

を扱います。

実行環境

Swift: 5.1.3
Xcode: 11.3.1

Selector完全攻略

NotificationCenterのAddObserver()の引数の中に、Selectorが出てきます。
このSelector、僕はUI部品にAddTarget()するときの引数で最初に遭遇して、かなり扱いに苦しみました。

addTarget例
let button = UIButton()
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

@objc func somefunction() { …… }

あるボタンをタップしたら、何か処理を動かすといったコードを書くとき、
こんな指定をすると思うんですが、正直疑問はたくさんありました。

  • @objcとは?
    • これはでもなんかお約束でつけなきゃいけないし、Xcodeが補完してくれるのでそんな問題じゃない
  • #Selectorってなんだ?
    • Selector()でもセレクタつくれるけど、どっち使ったらいいの?
  • なんか他のSwiftの構文と君全然違くない?
    • クロージャの渡し方と全然違うんだけど……
    • てか引数どうなってんの
    • これは何? 文字型? クロージャ?
  • たまにサンプルでsomefunction(_:)ってなってるの、何?
    • アメリカ式の顔文字?→何わろとんねん

当時の自分の疑問を解消するような形で、説明していきたいと思います。

Selectorとは

セレクタ(Selector)は、そもそもObjective-Cの概念です。
Swiftには純粋な意味でのセレクタは存在しませんが、
それだとObjective-Cのコードとの互換性で困るので、折衷案的にSwiftでもセレクタが使えます。

Objective-Cでは、下記のように指定します。

@selector ( method )

Swiftでは実は指定する方法が三通りあります。
よく使うと思われる順で、#Selector、Selector、文字列のみの三つです。

実装方法(引数なし)

セレクタで使おうとしているメソッドに引数がないときは指定が楽なので、まずはその前提で説明します。

共通サンプルコード

説明上必要なのはAddTarget()のところだけなんですが、動作確認用に使ったサンプルコード全部を載せときます。
自分で色々試してみたい方はお使いください。

サンプル
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100))
        button.backgroundColor = .black
        button.setTitle("Event", for: .normal)
        button.titleLabel?.textColor = .white
        view.addSubview(button)
        //以後の説明では↓この一行だけ使います
        button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
    }

    @objc func somefunction() {
        print("動かしたい処理を記述")
    }
}

#Selector

button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

詳しくは→Selector Expression

Selector

button.addTarget(self, action: Selector("somefunction"), for: .touchUpInside) //No method declared with Objective-C selector 'somefunction'というワーニングが出る

Swiftで#とか@とかの接頭詞がついていると、あまり利用が推奨されないものという感覚があって、
Selectorの方が#SelectorよりSwiftらしいのか? と勉強しはじめの頃は思ったのですが、
Selectorに関しては#Selectorを使う方がいいと思います。
(異論あればコメントください。#なしのSelectorの存在意義が今のところ僕の中ではイマイチ飲み込めてません……)

XcodeからWrap the selector name in parentheses to suppress this warningという修正案が出るので、Fixを押すと、

え……
button.addTarget(self, action: Selector(("somefunction")), for: .touchUpInside)

なぜか二重カッコに訂正されます。
まあそれはいい(よくないが)んですが、一番キツイのは、#Selectorと違って、メソッド名のコンパイラチェックがないことです。

#Selector
button.addTarget(self, action: #selector(samufankusyon), for: .touchUpInside)
// →Use of unresolved identifier 'samufankusyon'

#Selectorは、メソッド名のチェックをしてくれるので、コンパイルエラーとなります。
一方で、

Selector
button.addTarget(self, action: Selector(("samufankusyon")), for: .touchUpInside)

Selectorは文字列なので存在しないメソッド名でも指定できてしまい、実行時エラーとなります。

Selectorで文字列からメソッドを呼び出す(Swift4.2)

文字列のみ

実は文字列だけ突っ込んでも、ワーニングは出ますが、なんか上手いこと解釈してくれて動きます。

button.addTarget(self, action: "somefunction", for: .touchUpInside) 
//No method declared with Objective-C selector 'somefunction'
//Replace '"somefunction"' with 'Selector("somefunction")' [Fix]

動作的にはSelector("somefunction")と一緒ですね。

実装方法(引数あり)

で、引数があるときの書き方。
ちょっと冗長になりますが、3パターン全部書いてみようと思います。
(初学者の頃網羅的に書いてくれてる記事がなくて辛かった思い出があるので)

その前に

セレクタで指定したメソッドの引数に何を渡せるかを決めるのは、そのセレクタを引数にとる関数が決めます。
ここではAddTarget()です。

#selector に複数の引数を持たせたいです
↑こちらの回答にある通り、AddTarget()でとれる引数は最大2つで、
しかもイベント発生時のUIButtonとUIEventのインスタンスしか取れません。

したがって、

button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)

@objc func somefunction(some: String, number: Int) {
    print("動かしたい処理を記述")
}

こんな書き方をすると、実行時エラーとなります。

ちなみに、NotificationCenterのAddObserver()の引数は、Notificationインスタンス1つです。

aSelector
Selector that specifies the message the receiver sends observer to notify it of the notification posting. The method specified by aSelector must have one and only one argument (an instance of NSNotification).

変数の型と中身は制約がありますが、変数名とセレクタの書き方は自由です。
具体的に見ていきましょう!

#Selector

button.addTarget(self, action: #selector(somefunction(sender:forEvent:)), for: .touchUpInside)
//button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)
// ↑これでも呼び出し可

// sender/forEvent/eventなどの変数名は開発者によって変更可
@objc func somefunction(sender: UIButton, forEvent event: UIEvent) {
    print("動かしたい処理を記述")
    print(sender.titleLabel?.text) //Optional("Event")
    print(event) //<UITouchesEvent: 0x282e10640>……
}

初学者の頃よく混乱したネットに転がってるサンプルで、#selector(somefunction(_:))と指定している例がありますが、
あれは呼び出すメソッドの外部引数名を_で省略可としているため、こんな書き方ができるんですね〜

button.addTarget(self, action: #selector(somefunction(_:_:)), for: .touchUpInside)
@objc func somefunction(_ sender: UIButton, _ event: UIEvent) {……}

コロンで変数分けてるのがSwift的には気持ち悪いですが、Objective-Cの名残りだと思われます。

Selector  

Selectorの引数指定はちょっとしんどいです。
#Selectorみたいに、

button.addTarget(self, action: Selector("somefunction(sender:forEvent:)")), for: .touchUpInside)

でイケると思うじゃないですか。
実行時エラーでクラッシュします。
NSInvalidArgumentExceptionです。

Selectorで文字列からメソッドを呼び出す(Swift4.2)こちらを参考にしながら、色々試してみましたが、30分くらいクラッシュし続けました。

ダメだった例
Selector("somefunctionWithSender:ForEvent:")
Selector("somefunction")
Selector("somefunctionWithsender:forEvent:")
Selector("somefunctionwithsender:forEvent:")
Selector("somefunctionWithSender:Event:")
Selector("somefunctionWithSender:event:")
// 頭おかしくなりそう

メソッドの引数を一つにしてみたら動いたので、もうこれ書いてお茶を濁そうかなと思ったとき、参考にしていたページを見直して、気付きました。
あ、これ第二引数、一文字目小文字やん!!!!

正解
button.addTarget(self, action: Selector("somefunctionWithSender:forEvent:"), for: .touchUpInside)

正解はこう。
WithV1:V2:……みたいに引数を指定するのは、Objective-C方式です。
注意すべきは、第一引数はメソッド定義に関わらず一文字目を大文字に、
第二引数以降はメソッド定義に従う、というところでした。

うーんやっぱ#Selectorより無印Selectorの方がObjective-Cっぽいですよねえ。

文字列のみ

Selectorと一緒です。

NotificationCenter使うときのUserInfoの扱い方

AddTarget()を例に長々説明してきましたが、AddObserver()についても少し書かせてください。

AddObserver()はセレクタで指定したメソッドにNotification型を渡せるのですが、
NotificationにはuserInfoというプロパティがあり、postするときにちょっとしたデータを渡せます。

import Foundation

class Subject {
    let eventName: Notification.Name

    init(eventName: Notification.Name) {
        self.eventName = eventName
    }

    func post() {
        NotificationCenter.default.post(name: eventName, object: nil, userInfo: ["test": "Yeah"])
    }
}

class Observer {
    let eventName: Notification.Name

    init(eventName: Notification.Name) {
        self.eventName = eventName
        NotificationCenter.default.addObserver(self, selector: #selector(doWhenEventOccur), name: eventName, object: nil)
//        NotificationCenter.default.addObserver(self, selector: "doWhenEventOccurWithNotification:", name: eventName, object: nil)
    }

    @objc func doWhenEventOccur(_ notification: Notification) {
        print(notification.userInfo) //Optional([AnyHashable("test"): "Yeah"])
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

let eventName = Notification.Name("Event Occurs")
let observer = Observer(eventName: eventName)
let subject = Subject(eventName: eventName)
subject.post()

※このコードについての解説は前回の記事参照

ただ[AnyHashable: Any]型という、ちょっと扱いづらい辞書型で渡るので、正直使いづらいです。

初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について

以上で本題は終わりなんですが、関連して。
iOSアプリ開発に手を染めて、1〜2ヶ月くらい、やたらAddTarget()のときに値を渡そうとしてもがいた記憶があります。
今思うと、なんでそんな必要があったのかな〜という気持ちなんですが、
もしかしたらこの記事をみている方も、無理やりセレクタで指定したメソッドに値渡したくてこの記事にたどり着いたかもしれないので、
そのことについて書こうと思います。

結論:セレクタで変数渡したいときはプロパティ経由で渡すよう検討する

結論的にはこれです。
Qiitaを漁ると、userInfoで渡すとか、UIButtonをExtensionで拡張するとか、やり方は色々出てきます。

ただきれいなやり方とは言えないと思います。
ケースバイケースではありますが、クラスなり構造体なりのプロパティとして渡したい変数をつくって、そこ経由で受け渡すのがベストだと思います。
なんらかの理由で渡せないなら、アクセス制御の設計がミスっているので、そこを直した方がいいです。

なぜ初学者はセレクタに変数を渡したがるのか

などと正論を書いても、実際僕も最初の頃セレクタに変数渡さないと実装できなかったシーンが何かあったような記憶がうっすらとあります。
具体的なケースは思い出せないのですが、確かにありました。

なんでだろうな〜と考えたところ、僕の中で一つの結論が出ました。
初学者がセレクタに変数を渡したい、と思うのは、ViewControllerが世界の全てだからではないでしょうか。
Xcodeで新規プロジェクトつくった際に用意されるサンプルコードを使うと、ViewControllerのViewDidLoad()から書き始めることになります。

そこでコード量が多くなって、各処理をメソッドに切り分けていきます。
ViewControllerにはデフォルトだとイニシャライザが書かれていません。
なので、ViewControllerのプロパティを作ろうとすると、イニシャライザがないよ、で怒られます。

初期値を設定してやるとか、オプショナル型にするとか、イニシャライザを真面目に書くとか、
なんらか対処できればいいのですが、いかんせん初学者なので、
「ViewControllerに直でプロパティ書くとなんか上手くいかないなあ……」で終わる(僕の場合ですが)。
となると何がなんでもセレクタに変数を渡せないと、自分がやろうとしている機能ができない! となるわけです。

遠回りのようですが、iOSアプリ全体の構造とか、クラスの使い方とかについて勉強するのが良いのだと思われます。

まとめ

というわけで、SwiftのSelectorについてでした。

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

UICollectionViewでカルーセルを作る時の注意点

UICollectionViewでカルーセル

[iOS] UICollectionView のレイアウトクラスを作成して「左右のアイテムをチラ見せするレイアウト」を実現する
https://dev.classmethod.jp/smartphone/iphone/collection-view-layout-cell-snap/

上記から引用してSwiftで書き直したものが↓。

CarouselCollectionViewFlowLayout.swift
final class CarouselCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return .zero }
        let pageWidth = itemSize.width + minimumLineSpacing
        let currentPage = collectionView.contentOffset.x / pageWidth

        if abs(velocity.x) > 0.2 {
            let nextPage = (velocity.x > 0) ? ceil(currentPage) : floor(currentPage)
            return CGPoint(x: nextPage * pageWidth, y: proposedContentOffset.y)
        } else {
            return CGPoint(x: round(currentPage) * pageWidth, y: proposedContentOffset.y)
        }
    }
}

このCarouselCollectionViewFlowLayoutをCollectionView.layoutに渡してやれば良い。

isPagingEnabled = trueをやってはいけない

func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { ... }

collectionView.isPagingEnabled = trueを行うと、上記処理を通るものの、returnで返しているCGPointの値は無視された挙動をする。
これに何回か引っかかっている気がするので、備忘録がてら書いた。

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

Apple Music API(MusicKit)のDeveloper Token取得方法

いつも忘れてググって色んなサイト飛んだりするので、自分の備忘録として書いておく

Apple Developerページでの作業

https://developer.apple.com/account/

Music IDを取得

  1. Developer Accountの証明書等々の一覧ページにいく

  2. Identifiers+ボタンより Music IDsを選択

  3. Description, Identifierを入力し、作成

MusicKit用の秘密鍵を作成

  1. Keysページで+ボタンより作成

  2. Key Nameを入力し、Music Kitconfigureボタンで紐付けたいMusic IDを設定

  3. Registerボタンで作成し、Downloadボタンよりダウンロードしておく。 .p8ファイルがダウンロードされる

Key ID, TeamIDを確認

これに加え、先程作成した.p8 ファイルを用意

apple-music-token-generatorを使用してDeveloper Token作成

apple-music-token-generatorをダウンロードし、READMEに従いPython関連をインストール

  • music_token.pyの内容を自分の秘密鍵, Key ID, Team IDに書き換える

    秘密鍵 ... 先程ダウンロードした.p8ファイルの内容

  • ターミナル上でmusic_token.pyを実行

 python music_token.py
  • 出力されたTokenでApple Music APIにアクセスできる
   curl -v -H 'Authorization: Bearer 出力されたToken' "https://api.music.apple.com/v1/catalog/us/artists/36954" 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでデザインパターンを振り返る - Abstract factory pattern

デザインパターンについてもう一度振り返ろうと思いしばらく連載しようと思います。
今回は、Abstract factory patterです。

Abstract factory pattern

スクリーンショット 2020-01-29 22.45.49.png
直訳すると抽象的な工場パターンとなります。

お互いに関連したり依存し合うオブジェクト群を、その具象クラスを明確にせずに生成するためのインターフェースを提供することを目的とする。
- オブジェクト指向における再利用のためのデザインパターンより

特定のオブジェクトを生成するための共通インターフェースを継承したオブジェクト生成用のクラスを介して特定のオブジェクトを生成することで、オブジェクト生成時の責務の切り分けと過程の秘匿化が容易になります。

ベースとなるサンプル

class AbstractFactoryViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        /// john cooper works
        let miniJcw = Mini(power: PowerSpec.johnCooperWorks.rawValue,
                        bodyColor: "ブレイジングレッド")
        sayCarStatus(with: miniJcw)
    }

    /// 車のスペックをPrintします
    func sayCarStatus(with mini: Mini) {
        print("このMINIは、馬力が\(mini.power)。 車体色が、\(mini.bodyColor)です。")
    }

}

/// MINI
class Mini {
    /// 馬力
    let power: Int
    /// 車体色
    let bodyColor: String

    init(power: Int, bodyColor: String) {
        self.power = power
        self.bodyColor = bodyColor
    }
}

今回は上記の例を用います。
viewDidLoadのタイミングで、Miniオブジェクトを生成しています。
オブジェクト生成後、func sayCarSpec()を呼び車のスペックをプリントします。
現状では、View側にオブジェクト生成に必要な馬力と車体色がハードコーディングされています。
現状でも期待通りの車のスペックをプリントすることができます。

更に別の車のスペックも欲しい場合

class AbstractFactoryViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        /// john cooper works
        let miniJcw = Mini(power: PowerSpec.johnCooperWorks.rawValue,
                        bodyColor: "ブレイジングレッド")
        sayCarStatus(with: miniJcw)
        /// one
        let miniOne = Mini(power: PowerSpec.one.rawValue,
                           bodyColor: "ブリティッシュグリーン")
        sayCarStatus(with: miniOne)
    }

    /// 車のスペックをPrintします
    func sayCarStatus(with mini: Mini) {
        print("このMINIは、馬力が\(mini.power)。 車体色が、\(mini.bodyColor)です。")
    }

}

/// MINI
class Mini {
    /// 馬力
    let power: Int
    /// 車体色
    let bodyColor: String

    init(power: Int, bodyColor: String) {
        self.power = power
        self.bodyColor = bodyColor
    }
}

MINI OneのスペックをPrintするために、新しいMINIオブジェクトを生成するようにしました。
現状では、View側でオブジェクトの生成に必要な値がハードコードされています。もし、MINIオブジェクトの生成に必要な引数などが増えた場合、JohnCooperWorksオブジェクトとminiOneオブジェクトに渡す引数を増やす必要があり、オブジェクト側の変更が難しくなってしまいます。
また、View側の責務は受け取ったデータをUIに反映することであるため、データの生成・取得に関しては責務外のことを上記のサンプルでは行っています。

Abstract factory patternを適用する

class AbstractFactoryViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        /// john cooper works
        let miniJcw = JohnCooperWorksFactory().create()
        sayCarStatus(with: miniJcw)
        /// one
        let miniOne = OneFactory().create()
        sayCarStatus(with: miniOne)
    }

    /// 車のスペックをPrintします
    func sayCarStatus(with mini: Mini) {
        print("このMINIは、馬力が\(mini.power)。 車体色が、\(mini.bodyColor)です。")
    }

}

/// Factoryのルールを定義したProtocol
protocol MiniFactoryProtocol {

    /// Miniオブジェクトを生成する
    func create() -> Mini

}

/// JohnCooperWorksオブジェクトを生成するオブジェクト
class JohnCooperWorksFactory: MiniFactoryProtocol {

    /// JohnCooperWorksの馬力
    private let johnCooperWorks = 231

    /// Miniオブジェクトを生成する
    func create() -> Mini {
        return Mini.init(power: johnCooperWorks,
                         bodyColor: "ブレイジングレッド")
    }

}

/// Mini Oneオブジェクトを生成するオブジェクト
class OneFactory: MiniFactoryProtocol {
    /// Oneの馬力
    private let one = 102

    func create() -> Mini {
        return Mini.init(power: one,
                         bodyColor: "ブリティッシュグリーン")
    }
}

実際にAbstract factory patternを適用してみた例が上記になります。
適用前のサンプルでは、View側でMiniオブジェクト生成に必要な値をMiniオブジェクトのイニシャライズ時の引数として渡してMiniオブジェクトを生成していました。
それを車のグレードごとにMiniオブジェクトを生成するファクトリークラスを用意し、グレードごとの馬力もファクトリークラス内で保持するように変更しています。

また、protocolを定義することで、どのFactoryクラスにもfunc create() -> Miniメソッドが定義されていることを保証しています。

メリット

柔軟な変更性

例えば、突如オプションパッケージを車に設定できるようにする仕様変更があったとします。
そんな仕様変更を受け入れるためにMiniオブジェクトを下記のように変更することになりました。

/// MINI
class Mini {
    /// 馬力
    let power: Int
    /// 車体色
    let bodyColor: String
    /// オプションパッケージ名
    let optionPackageName: String?

    init(power: Int, bodyColor: String, packageName: String? = nil) {
        self.power = power
        self.bodyColor = bodyColor
        self.optionPackageName = packageName
    }
}

オプションパッケージ名のプロパティが増えたのと、イニシャライザーの引数が追加されています。
イニシャライザーの引数は、従来の実装を考慮しpackageNameを設定しない場合はデフォルトの値をnilとします。

class AbstractFactoryViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        /// john cooper works
        let miniJcw = Mini(power: PowerSpec.johnCooperWorks.rawValue,
                        bodyColor: "ブレイジングレッド", packageName: "ペッパーパッケージ")
        sayCarStatus(with: miniJcw)
        /// one
        let miniOne = Mini(power: PowerSpec.one.rawValue,
                           bodyColor: "ブリティッシュグリーン")
        sayCarStatus(with: miniOne)
    }

    /// 車のスペックをPrintします
    func sayCarStatus(with mini: Mini) {
        print("このMINIは、馬力が\(mini.power)。 車体色が、\(mini.bodyColor)です。")
        if let packageName = mini.optionPackageName {
            print("パッケージは、\(packageName)が標準装備です。")
        }
    }

}

JohnCooperWorkモデルのみにパッケージ名をもたせるためにView側で行っているオブジェクトのイニシャライズ時に引数として新たにパッケージ名を追加で渡す必要が発生します。
このような変更が生じるたびにView側に記載されたオブジェクト生成の処理を変更する必要が発生するため、改修が難しくなってしまいます。

Abstract factory patternを適用し、JohnCooperWorksのステータス値を持ったMINIオブジェクトを生成する場合は、JohnCooperWorksFactoryクラス内でMINIオブジェクトのイニシャライズ時にパッケージ名を引数で追加するようにするだけで実現できます。

/// JohnCooperWorksオブジェクトを生成するオブジェクト
class JohnCooperWorksFactory: MiniFactoryProtocol {

    /// JohnCooperWorksの馬力
    private let johnCooperWorks = 231

    /// Miniオブジェクトを生成する
    func create() -> Mini {
        return Mini.init(power: johnCooperWorks,
                         bodyColor: "ブレイジングレッド",
                         packageName: "ペッパーパッケージ"
        )
    }

}

/// Mini Oneオブジェクトを生成するオブジェクト
class OneFactory: MiniFactoryProtocol {
    /// Oneの馬力
    private let one = 102

    func create() -> Mini {
        return Mini.init(power: one,
                         bodyColor: "ブリティッシュグリーン")
    }
}

JohnCooperWorksオブジェクト、Mini Oneオブジェクトを生成するのに必要な値やオブジェクトを変更する必要がある場合は、それぞれのFactoryクラスの処理を変更するだけで済むようになるため後からの変更性が容易になります。
また、オブジェクトを生成するための処理が完全にFactoryクラス内に閉じられ過程が隠蔽されることで責務の分担も明確になります。

デメリット

新たな部品の種類に対応することが少し面倒

Factoryクラスは、オブジェクトを生成するために必要な値やオブジェクトの集まりを固定化しているだけに過ぎないため、上記のような追加のプロパティが定義されたときにFactoryクラスの修正や今回は例として扱っていませんが子のオブジェクトクラスまで修正する必要が発生します。
ただ、これに関しては拡張性を考慮して実装すればこと足りることなのであまり大きな問題にはなりません。
初期実装の段階から拡張する必要が生まれた場合は、デフォルト引数などを用いて追加プロパティを初期化することも出来ますので考え方次第で、デメリットは解消することが出来ます。

実務での活用事例など

シンプルかつイメージが少し湧きづらいサンプルを用いて説明してきましたが、実際に実務で普段使っているイメージとしては、Translatorクラスなどが該当すると思います。

class AIUFilePathDataModelTranslator {

    func translate (with value: String, query: String) -> [AIUFilePathDataModel] {
        let lines = value.components(separatedBy: .newlines)
        return lines.reduce([AIUFilePathDataModel]()) {
            var result = $0
            guard let pathData: AIUFilePathDataModel = createPathData(with: $1, query: query) else {
                return result
            }
            result.append(pathData)
            return result
        }
    }
}

上のコードは、趣味で開発しているframework内で利用しているTranslatorクラスです。
FlashAirから返却されるcsv形式の文字列を改行コードでスプリットして配列化、各配列の値から更に分解し、AIUFilePathDataModelオブジェクトを生成して複数のオブジェクトを入れた配列として結果を返却するものです。
やってることとしては、必要な値を受け取り、イニシャライズに必要な値に変換して、オブジェクトをイニシャライズして返却するということです。
これがAbstract factory patternだと自分の中では解釈しています。

おわりに

普段当たり前に書いているコードですが、改めてデザインパターンに向き合うとこんなとこにこれが活用されているんだなと振り返る機会になりました。
知見の洗い直しも兼ねてしばらく振り返りの機会として連載続けていきたいなと思います。
多分認識の違いもあったりすると思うので、マサカリ飛ばしてください!

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

UITableViewCell の高さを UIImage のアスペクト比を元に決定する方法

はじめに

UITableViewCell にセットする画像のアスペクト比が固定ではない場合、セルの高さの決め方を工夫する必要があります。今回はその方法の一つとして、 AutoLayout を用いた解決策を示します。

解決策

final class CustomTableViewCell: UITableViewCell {
     @IBOutlet private weak var mainImageView: UIImageView!

     private var aspectConstraint: NSLayoutConstraint? {
         didSet {
             if let value = oldValue {
                 mainImageView.removeConstraint(value)
             }
             if let constraint = aspectConstraint {
                 mainImageView.addConstraint(constraint)
             }
         }
     }

     func prepare(image: UIImage) {
         setImage(image)
     }

     private func setImage(_ image: UIImage) {
         let aspect = image.size.width / image.size.height

         let constraint = NSLayoutConstraint(
             item: mainImageView as Any,
             attribute: .width,
             relatedBy: .equal,
             toItem: mainImageView,
             attribute: .height,
             multiplier: aspect,
             constant: 0.0
         )
         constraint.priority = UILayoutPriority(rawValue: 999)
         aspectConstraint = constraint

         mainImageView.image = image
     }
 }

aspectConstraint: NSLayoutConstraint? ではセルの再利用を考慮して制約の削除も行っています。また、セルに対して複数回画像がセットされる可能性がある場合(セルのイニシャライズが行われてから破棄されるまでの間に複数回 setImage() が呼ばれる場合)にも対応します。

実際に制約を作成して UIImageView に適応するのが setImage() になります。 priority に関しては、構築している UI に応じて臨機応変に設定してください。

参考文献

Dynamic UIImageView Size Within UITableView

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