20210228のiOSに関する記事は4件です。

TodoAPPでRxSwift入門[part1]

概要

最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとは

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。

とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)

今回使用するライブラリ

RxSwift pod 'RxSwift'
--- ObservableなどのRx系の本領的なものを使うのに必要
RxCocoa pod 'RxCocoa'
--- UIKitでRxを使うのに必要
RxDataSources pod 'RxDataSources'
--- tableViewなどのdatasourceの扱いを楽にしてくれる
Firestore pod 'Firebase/Firestore'
--- todoの保存をするのに必要

以上のライブラリを使用していきます。

認証機能は?と思われたかもしれないですが今回は省かせていただきます。

また、今回の記事はfirebaseやpodは導入済みのものとして話を進めさせていただきます。

StoreManager

まずはこのアプリのもっとも重要なTodoの追加や取得の処理を書いていきます。
StoreManagerという名前のファイルを作ってください。

StoreManager.swift
import Foundation
import FirebaseFirestore
import RxSwift

class StoreManager {
    static let shared = StoreManager()
    private let store = Firestore.firestore()
}

こんな感じで定義してください。
ここは説明するまでもないと思うので説明を省略します。

次にTodoを保存する関数を作ります。
extensionで拡張すると可読性が上がるので拡張します。

StoreManager.swift
extension StoreManager {
    // firestore にtodoのデータを保存
    func insertTodoToFireStore(title: String, detail: String) {
        let data: [String: Any] = [
            "title": title,
            "detail": detail,
            "createdAt": Timestamp()
        ]
        store.collection("todos").addDocument(data: data)
    }
}

ここも特に説明はいらないと思いますは簡単に説明しておきます。
title,detailを受け取り[String: Any]型のDictionaryにして各値を格納します。そして、todosというコレクション名のコレクションにデータを保存します。

次にデータを取得する関数を作ります。

StoreManager.swift
// firestore からtodosのデータ取得
    func fetchTodosFromFirestore() -> Single<[TodoModel]> {
        Single.create { [weak self](single) -> Disposable in
            self?.store.collection("todos").getDocuments { (snapshots, error) in
                guard let docs = snapshots?.documents, error == nil else {
                    single(.failure(CustomError.error(message: "Failed To Fetch Todos From Firestore")))
                    return
                }
                let todos = docs.map { TodoModel(data: $0.data()) }
                single(.success(todos))
            }

            return Disposables.create()
        }
    }

ここがこのクラスのキモですね。
戻り値にSingle<[TodoModel]>とありますね。
SingleはRxSwiftをインポートしていないと使えないので忘れないでください。
また、現時点ではTodoModelというものが定義されていないのでエラーになると思いますが気になる人はとりあえずTodoModelを定義しておいてください。

Singleについて説明していきます。
SingleはSuccessとErrorを流し、Completedを流さないものになります。
successとはnextを一回だけ流れるものでerrorはそのままですね。
ほーという感じだと思いますがcomplete処理がいらなかったらSingleを使うという感じでいいんじゃないでしょうか。

Singleの他にもMaybe、Completableがあります。
詳しいことは以下の記事を参照してください。
RxSwift 3.3.0で追加された3つのUnit(Single, Maybe, Completable)

次にguard let文の中でエラー処理を書いています。
ここは説明なしでもわかると思います。
そして、取得した値をTodoModelに加工してsuccessで流してあげます。

最後にDisposables.create()ですが、これはストリームのライフサイクルを管理するためのものです。
disposeされたときに、ここに書いた処理が実行されます。
Disposableについて

TodoModel

先ほど飛ばしたTodoModelはこんな感じで書きます。

TodoModel.swift
import Foundation
import FirebaseFirestore

struct TodoModel {
    let title: String
    let detail: String
    let createdAt: Timestamp

    init(data: [String: Any]) {
        title = data["title"] as? String ?? ""
        detail = data["detail"] as? String ?? ""
        createdAt = data["createdAt"] as? Timestamp ?? Timestamp()
    }
}

最後に

part1はここまでになります。
正直まだ理解しきれていないこともあり、調べながらになりますが理解できているところとあまり理解できていないとこがわかってoutputって大切だなと思いました。
また、あまり理解できずに書いてしまっているところや誤解釈してしまっているところがありましたらご指摘の方お願いいたします。

[part2]はViewModelの作成をしていきます。
TodoAPPでRxSwift入門[part2]

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

RxSwiftでTodoAPP入門[part1]

概要

最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとは

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。

とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)

今回使用するライブラリ

RxSwift pod 'RxSwift'
--- ObservableなどのRx系の本領的なものを使うのに必要
RxCocoa pod 'RxCocoa'
--- UIKitでRxを使うのに必要
RxDataSources pod 'RxDataSources'
--- tableViewなどのdatasourceの扱いを楽にしてくれる
Firestore pod 'Firebase/Firestore'
--- todoの保存をするのに必要

以上のライブラリを使用していきます。

認証機能は?と思われたかもしれないですが今回は省かせていただきます。

また、今回の記事はfirebaseやpodは導入済みのものとして話を進めさせていただきます。

StoreManager

まずはこのアプリのもっとも重要なTodoの追加や取得の処理を書いていきます。
StoreManagerという名前のファイルを作ってください。

StoreManager.swift
import Foundation
import FirebaseFirestore
import RxSwift

class StoreManager {
    static let shared = StoreManager()
    private let store = Firestore.firestore()
}

こんな感じで定義してください。
ここは説明するまでもないと思うので説明を省略します。

次にTodoを保存する関数を作ります。
extensionで拡張すると可読性が上がるので拡張します。

StoreManager.swift
extension StoreManager {
    // firestore にtodoのデータを保存
    func insertTodoToFireStore(title: String, detail: String) {
        let data: [String: Any] = [
            "title": title,
            "detail": detail,
            "createdAt": Timestamp()
        ]
        store.collection("todos").addDocument(data: data)
    }
}

ここも特に説明はいらないと思いますは簡単に説明しておきます。
title,detailを受け取り[String: Any]型のDictionaryにして各値を格納します。そして、todosというコレクション名のコレクションにデータを保存します。

次にデータを取得する関数を作ります。

StoreManager.swift
// firestore からtodosのデータ取得
    func fetchTodosFromFirestore() -> Single<[TodoModel]> {
        Single.create { [weak self](single) -> Disposable in
            self?.store.collection("todos").getDocuments { (snapshots, error) in
                guard let docs = snapshots?.documents, error == nil else {
                    single(.failure(CustomError.error(message: "Failed To Fetch Todos From Firestore")))
                    return
                }
                let todos = docs.map { TodoModel(data: $0.data()) }
                single(.success(todos))
            }

            return Disposables.create()
        }
    }

ここがこのクラスのキモですね。
戻り値にSingle<[TodoModel]>とありますね。
SingleはRxSwiftをインポートしていないと使えないので忘れないでください。
また、現時点ではTodoModelというものが定義されていないのでエラーになると思いますが気になる人はとりあえずTodoModelを定義しておいてください。

Singleについて説明していきます。
SingleはSuccessとErrorを流し、Completedを流さないものになります。
successとはnextを一回だけ流れるものでerrorはそのままですね。
ほーという感じだと思いますがcomplete処理がいらなかったらSingleを使うという感じでいいんじゃないでしょうか。

Singleの他にもMaybe、Completableがあります。
詳しいことは以下の記事を参照してください。
RxSwift 3.3.0で追加された3つのUnit(Single, Maybe, Completable)

次にguard let文の中でエラー処理を書いています。
ここは説明なしでもわかると思います。
そして、取得した値をTodoModelに加工してsuccessで流してあげます。

最後にDisposables.create()ですが、これはストリームのライフサイクルを管理するためのものです。
disposeされたときに、ここに書いた処理が実行されます。
Disposableについて

TodoModel

先ほど飛ばしたTodoModelはこんな感じで書きます。

TodoModel.swift
import Foundation
import FirebaseFirestore

struct TodoModel {
    let title: String
    let detail: String
    let createdAt: Timestamp

    init(data: [String: Any]) {
        title = data["title"] as? String ?? ""
        detail = data["detail"] as? String ?? ""
        createdAt = data["createdAt"] as? Timestamp ?? Timestamp()
    }
}

最後に

part1はここまでになります。
正直まだ理解しきれていないこともあり、調べながらになりますが理解できているところとあまり理解できていないとこがわかってoutputって大切だなと思いました。
また、あまり理解できずに書いてしまっているところや誤解釈してしまっているところがありましたらご指摘の方お願いいたします。

[part2]はViewModelの作成をしていきます。

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

【SwiftUI】contextMenuで長押ししたときにメニューを出す

先日リリースしたアプリに使用した技術をひとつずつ解説しています。
私のアプリはこちら。

contextMenuとは

任意のビューに対し、長押ししたときにメニューを出す機能を追加するメソッド。

実際の動作

実際にアプリを触らないとわかりづらいかもしれませんが、長押しすることでメニューが表示されています。また、メニューの外側をタップするとメニューが閉じるようになっています。
iOSアプリでよく見かける機能ではないでしょうか。

ソースコード

ContentView.Swift
import SwiftUI

struct ContentView: View {
    let evs = ["イーブイ", "ブースター", "シャワーズ", "サンダース", "エーフィ", "ブラッキー", "グレイシア", "リーフィア", "ニンフィア"]
    let colors1: [Color] = [Color(#colorLiteral(red: 0.7254902124, green: 0.4784313738, blue: 0.09803921729, alpha: 1)), .red, .blue, .yellow, Color(#colorLiteral(red: 0.8446564078, green: 0.5145705342, blue: 1, alpha: 0.5)), .black, Color(#colorLiteral(red: 0.6872052062, green: 0.9808971643, blue: 1, alpha: 1)), Color(#colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1)), .pink]
    let colors2: [Color] = [.white, .white, .white, .black, .black, .yellow, .blue, Color(#colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1)), Color(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1))]

    @State var evID = 0

    var body: some View {
        ZStack{
            Circle()
                .foregroundColor(colors1[evID])
                .frame(width: 250)
            VStack{
                Image(systemName: "sparkles") //このビューを長押しするとメニューが現れるようにする
                    //ここからがコンテキストメニュー
                    .contextMenu(menuItems: {
                        ForEach(0..<9) { n in
                            Button(action: {
                                evID = n
                            }, label: {
                                Text(evs[n])
                            })
                        }
                    })
                    //ここまでがコンテキストメニュー
                Text(evs[evID])
                    .padding()
            }
            .font(.title)
            .foregroundColor(colors2[evID])
        }
    }
}

「長押しするとメニューが出る」という機能を追加したいビューに対して.contextMenuを書きます。
そして、contextMenuの引数であるmenuItemsの{}内が、表示されるメニューの内容になります。

menuItemsには何を書く?

contextMenuでTextSF Symbolsを使ったImageを表示させることができます。しかし、書いたものを自動的にボタンにする、という機能はありません。上のサンプルアプリのようにcontextMenuにボタンを表示させたい場合は、menuItemsにはButtonビューを書くのが良いでしょう。

まとめ

簡単なコードでおもしろくて便利な機能を実現できます。ぜひ、SwiftUIでcontextMenuを使ってみてください。

よかったらTwitterのフォローをお願いします。
https://twitter.com/masayoshi_tozan

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

アルバイト先のiOSアプリをリファクタリングした

アルバイト先の会社で、プロジェクトの整備、リファクタリングをしようということになりました。

アプリのプロジェクトはこれまでオフショアで海外に渡ったり、業務委託やアルバイトに渡ったりと、カオス状態でした。

今回は、まとまった時間を設けてこのレガシープロジェクトを少しでも改善しようということだったので、最近学んだ内容をアウトプットするいい機会だと思って色々やってみました。

「これまでそんな事もやってなかったのか」と思われるかもしれませんが、
誤りや改善点等ありましたら、ご指摘いただければ幸いです。

GitLabからGitHubへの移行

これまでGitLabで管理されていたプロジェクトをGitHubに移行することになりました。

私はREADMEやWikiの整備を行いました。

具体的には以下を含めました。

README
  • 開発環境
  • ライブラリ管理
  • セットアップ方法
  • API仕様書
Wiki
  • 簡単なディレクトリ構成
  • 主な使用ライブラリ
  • Gitやコードの命名規則

また、ブランチの運用も変更しました。

  • Main(リリース用)
  • Staging(テスト用)
  1. Mainからブランチを切って、作業用ブランチはStagingにマージ
  2. Stagingでテスト完了後にリリースバージョンのタグをつけてマージ
  3. Mainをリリース

rbenv で ruby のバージョンを指定

CocoaPods は ruby で作られたツールです。

チーム開発を行う場合、各々の PC の ruby のバージョンが異なるとライブラリのインストールに失敗したり、上手く動かないことがあるので rbenv を使って ruby のバージョンを統一しました。

bundler など他にもツールが出てきて一気に理解するのは大変でしたが、以下の記事では丁寧に解説されています。

Cocoapods と Carthage を共存させた iOS プロジェクトの始め方

SwiftLint の導入

SwiftLintを導入しました。

ルールに関しては、業務委託の方が普段使用されている.swiftlint.yml ファイルが導入されました。

それに少しルールを追加しただけで、かなり緩めにルールが設定されています。

下記の記事を参考にさせていただきました。

Swiftの静的解析ツール「SwiftLint」のセットアップ方法

IQKeyboardManager の導入

これに関しては、本当に入れるだけで障壁はほぼありませんでした。

IQKeyboardManager

リソース管理、R.swiftの導入

他言語対応されているはずなのに、NSLocalizedString が使用されずにハードコーディングされている箇所があったりと、こちらもカオス状態でした。

この機会なのでリソース管理のライブラリを入れようということで、導入がSwiftGenより簡単なR.swiftを導入しました。

注意点としては、生成された R.generated ファイルが、SwiftLint の制約エラーを起こすことがあるので、R.generatedファイルは .swiftlint.yml ファイルの excluded: に含める必要があります。

また、“キーワードが被っているという問題で、生成をスキップします“というような警告が出たので、リソース側の整理が必要でした。

現在は文字列しかほぼ書き換えられてないので、今後は画像、ストーリーボード、セルなども書き換える必要があります。

下記の記事を参考にさせていただきました。

R.swiftのセットアップ方法(Swift5)

リテラルが結構使用されていたのでこちらも。

R.Swiftでは文字リソースで「%@」を使うと、その部分を引数にとる関数を自動生成する

継承が想定されないクラスには final をつける

Swift ではクラスを継承することができますが、自作のクラスで他のクラスから継承しないものには final をつけるべきです。

自分が継承されない想定で作ったクラスを他の人が継承する実装にしてしまったというようなことがチーム開発ではあり得るので、それを防ぐためには final を適切に設定することが必要です。

アクセスレベルを適切に設定

変数やメソッドなどには private などのアクセスレベルを設定することができます。 例えばクラス内でのみ使用し、クラスの外から呼ばれることのないメソッドは private にすることができます。

final と同様に、アクセスレベルも適切に設定することが必要です。

他人がプロジェクトのコードを読むときに「このメソッドは private だから外から呼ばれることはないんだ」と判断できるので、迅速な理解に繋がるというメリットもあると思います。

下記の記事で、非常にわかりやすく説明されています。

アクセス修飾子を理解する

delegate メソッドを extension に分ける

ViewController の定義時に全ての Delegate を継承せず適切に extension で分けました。

主に UITableViewDelegate や UICollectionViewDelegate
、 UITextFieldDelegate のメソッドがそのままクラス内に書かれており、定義されているメソッドが delegate メソッドなのか独自のメソッドなのか分かりづらい状態でした。

こちらは自分で簡単にまとめたものを参考にしました。

extensionを用いて可読性を上げる

不要なコメントアウト、 print デバッグの削除

使用されていないコードのコメントアウトや、 print 文がそのまま残されている状態でした。

print 文の中で可能なものは、下記の記事を参考に os.Logger で置き換えられました。

忙しい人向けの Explore logging in Swift - #WWDC20

例:

let logger = Logger()

// 省略

do {
    // 処理
} catch {
    logger.error(error)
}

今後やりたいこと

可能なところは StackView を使用

UI のメンテナンスの際は、強力な StackView である方がメンテナンスがやりやすいので、
可能なところは StackView を使用したいです。

可能なところStoryboard Reference を使用

Storyboard が入り組んでいると、メンテナンスがやりづらいと思います。

特にチーム開発ではコンフリクトの原因にもなるので、可能なところは StoryboardReference で Storyboard を分けるべきだと思います。

SwiftFormat の導入

機械的にコーディングルールを適用させれば楽になるかと思います。

ネストの解消

可読性の低いネストが散見されるので、可能な限り guard 文などで可読性を改善したいです。

まとめ

本来であれば、重複した処理を共通化したり、クラスの抽象化をしたりと、より高度なことをやりたいですが、現状の私のレベルで出来ることはこのくらいでした。

やるべき事は山積みですが、引き続きプロジェクトに貢献していきたいです。

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