20201124のSwiftに関する記事は6件です。

Swift Decodable(Codable)  色んなJSONデータパターンのまとめ 【備忘録】

自分がAPIから取得したJSONのレスポンスパラメータで、色々な構造パターンがあったので備忘録も兼ねてまとめてみたいと思います。

そのままプロパティに適用できるパターン

一番手間がない形式でシンプルにデコード用クラスを定義できる

var data = """
{
  "id": 1,
  "name": "Taro",
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: String
}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

キーとプロパティが一致しないパターン

Swiftの変数名の命名規則はローワーキャメルケースなので、CodingKeyを使って揃えることができる

var data = """
{
  "ID": 1,
  "Name": "Taro",
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: String

    enum CodingKeys: String, CodingKey {
        case id = "ID"
        case name = "Name"
    }
}

let user: User = try JSONDecoder().decode(User.self, from: data)

print(user)

データ構造に配列があるパターン

デコードメソッドの引数に配列で渡す

var data = """
[
    {
        "id": 0,
        "name": "Mike"
    },
    {
        "id": 1,
        "name": "John"
    },
    {
        "id": 2,
        "name": "Mary"
    }
]
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: String
}

let user: [User] = try JSONDecoder().decode([User].self, from: data)

print(user)

ネストが深いパターン(自動デコード)

別のデコード用のstructを作る(あんまり自動で済むパターンないですが)

var data = """
{
    "id": 0,
    "name": {
        "last": "Suzuki",
        "first": "Taro"
    }
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name:Name

    struct Name: Decodable {
        let last: String
        let first: String
    }
}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

ネストが深いパターン(手動デコード)

おそらくこっちのがよくあるパターン 別のstructを作らずに済む
取得しない値(キー用意していない変数)は 辞書型ならnestedContainer、 配列ならnestedUnkeyedContainerで取得できる

階層ごとに別のCodingKeyを用意してあげる

var data = """
{
    "id": 0,
    "name": {
        "last": "Suzuki",
        "first": "Taro"
    }
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let lastName: String
    let firstName: String

    enum CodingKeys: String, CodingKey {
        case id
        case name
    }

    enum NameKeys: String, CodingKey {
        case lastName = "last"
        case firstName = "first"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)

        let nameValues = try values.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        lastName = try nameValues.decode(String.self, forKey: .lastName)
        firstName = try nameValues.decode(String.self, forKey: .firstName)
    }
}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

キーが存在しない時があるパターン (自動デコード)

プロパティをオプショナル型にする

var data = """
{
    "id": 0
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: String?
}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

キーが存在しない時があるパターン (手動デコード)

decodeIfPresentif let (変数) = try? decode~~ などを使って、もしキーが存在すればのデコードをする

var data = """
{
    "id": 0
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: String

    enum CodingKeys: String, CodingKey {
        case id
        case name
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
    }
}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

取得しない値が配列になっているパターン (手動デコード)

あんまりQiitaで見かけないのでこのパターンも記載(JSONデータの例が雑ですみません :pray:)

var data = """
{
    "id": 0,
    "names": [
        {
            "name": "太郎(日本名)"
        },
        {
            "name": "テンプレマン(ニックネーム)"
        },
        {
            "name": "Taro(アメリカネーム)"
        }
    ]
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: [String]

    enum CodingKeys: String, CodingKey {
        case id
        case names
    }

    enum NameKeys: String, CodingKey {
        case name
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)

        var names = try values.nestedUnkeyedContainer(forKey: .names)
        var nameArray: [String] = []

        while !names.isAtEnd {
            let nameDic = try names.nestedContainer(keyedBy: NameKeys.self)
            let nameValue = try nameDic.decode(String.self, forKey: .name)
            nameArray.append(nameValue)
        }
        name = nameArray
    }

}

let user: User = try JSONDecoder().decode(User.self, from: data)
print(user)

どっちかのキーで返ってくるパターン

EnumにDecodableを準拠させたプロパティを用意して、それを使って分岐で両方デコードする

var data1 = """
{
    "id": 0,
    "name": "Taro"
}
""".data(using: .utf8)!

var data2 = """
{
    "id": 1,
    "name": {
        "last": "Suzuki",
        "first": "Ichiro"
    }
}
""".data(using: .utf8)!

struct User: Decodable {
    let id: Int
    let name: NameType

    enum CodingKeys: String, CodingKey {
        case id
        case name
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        name = try values.decode(NameType.self, forKey: .name)
    }

    enum NameType: Decodable {
        case firstNameOnly(String)
        case fullName(Name)

        init(from decoder: Decoder) throws {
            let nameType = try decoder.singleValueContainer()

            if let nameValue = try? nameType.decode(Name.self) {
                self = .fullName(nameValue)
            }
            else {
                let nameValue = try nameType.decode(String.self)
                self = .firstNameOnly(nameValue)
            }
        }
    }
}

struct Name: Decodable {
    let last: String
    let first: String

    private enum NameKeys: String, CodingKey {
        case last
        case first
    }

    init(from decoder: Decoder) throws {
        let nameValues = try decoder.container(keyedBy: NameKeys.self)
        last = try nameValues.decode(String.self, forKey: .last)
        first = try nameValues.decode(String.self, forKey: .first)
    }
}

let user1: User = try JSONDecoder().decode(User.self, from: data1)
print(user1)
let user2: User = try JSONDecoder().decode(User.self, from: data2)
print(user2)

感想

もし記載ミス等あればご指摘お願いします :bow: (JSONデータ例が雑なのは悪しからず。。)
パースって大変ですよね(白目)

追記

仕事時に参考にしていた記事
【Swift】Codableについて備忘録

レスポンスのJSONが異なるAPIにオプショナルを使わず対応する方法(Swift)
Codableについて色々まとめた[Swift4.x]

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

【Swift】 TableViewCell に画像を入れる【初心者】

はじめに

TableViewCellに画像を入れる方法について調べたので、メモとしてまとめる事にしました。

storyboardにTableViewとTableViewCellを準備

TableView.png

cellにIdentifierを付ける

今回はIdentifierは「cell」にしました。
cell.png

Assets.xcassetsに画像ファイルを入れる

animal.png

TableViewをdatasouceとdelegateに繋ぐ

スクリーンショット 2020-11-23 13.17.25.png

UITableViewDelegate,UITableViewDatasourceを追加

ViewController.swift
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {

配列を準備

ViewController.swift
 let animalArray = ["虎", "猫","犬"]
    //画像用
 let imageArray = ["lion", "cat", "dog"]

セルの設定

ViewController.swift
 //セルの個数を指定する
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return animalArray.count

    }

    //セルに値を設定する
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        //セルを取得
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        //セルに表示する値を設定
        //テキストラベル
        cell.textLabel?.text = animalArray[indexPath.row]
        return cell
    }

スクリーンショット 2020-11-23 14.38.15.png

①1つの画像を全てのcellに入れる

ViewController.swift
 //①画像(1つの画像を全てのcellに入れる)
        cell.imageView?.image = UIImage(named: "lion")

スクリーンショット 2020-11-23 14.37.52.png

②cellの1つずつに別の画像を入れる

ViewController.swift
 //②画像を複数入れる
        cell.imageView?.image = UIImage(named: imageArray[indexPath.row])

スクリーンショット 2020-11-23 14.37.28.png

③cellの1つずつに別の画像を入れる(同じサイズにリサイズ)

CGSizeのwidth:50,height:50を変更する事で画像サイズを変更できます。

ViewController.swift
//③画像を同じサイズにリサイズ
        cell.imageView?.image = UIImage(named: imageArray[indexPath.row])?.resize(size: CGSize(width: 50, height: 50))

スクリーンショット 2020-11-23 14.36.56.png

リサイズメソッド

ViewController.swift
extension UIImage {

    func resize(size: CGSize) -> UIImage {
        let widthRatio = size.width / self.size.width
        let heightRatio = size.height / self.size.height
        let ratio = (widthRatio < heightRatio) ? widthRatio : heightRatio
        let resizedSize = CGSize(width: (self.size.width * ratio), height: (self.size.height * ratio))
        // 画質を落とさないように以下を修正
        UIGraphicsBeginImageContextWithOptions(resizedSize, false, 0.0)
        draw(in: CGRect(x: 0, y: 0, width: resizedSize.width, height: resizedSize.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage!
    }
}

全てのコード

①②のコードはコメントアウトしてあります。

ViewController.swift
import UIKit

class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {

    let animalArray = ["虎", "猫","犬"]
    //画像用
    let imageArray = ["lion", "cat", "dog"]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    //セルの個数を指定する
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return animalArray.count

    }

    //セルに値を設定する
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        //セルを取得
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        //セルに表示する値を設定
        //テキストラベル
        cell.textLabel?.text = animalArray[indexPath.row]
        //①画像(1つの画像を全てのcellに入れる)
//        cell.imageView?.image = UIImage(named: "lion")

        //②画像を複数入れる
//        cell.imageView?.image = UIImage(named: imageArray[indexPath.row])

        //③画像を同じサイズにリサイズ
        cell.imageView?.image = UIImage(named: imageArray[indexPath.row])?.resize(size: CGSize(width: 50, height: 50))

        return cell
    }
}

//リサイズメソッド
extension UIImage {

    func resize(size: CGSize) -> UIImage {
        let widthRatio = size.width / self.size.width
        let heightRatio = size.height / self.size.height
        let ratio = (widthRatio < heightRatio) ? widthRatio : heightRatio
        let resizedSize = CGSize(width: (self.size.width * ratio), height: (self.size.height * ratio))
        // 画質を落とさないように以下を修正
        UIGraphicsBeginImageContextWithOptions(resizedSize, false, 0.0)
        draw(in: CGRect(x: 0, y: 0, width: resizedSize.width, height: resizedSize.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage!
    }
}

終わりに

初学者のため、もし間違いがあれば教えていただければ幸いです。

参考サイト

https://qiita.com/ryokosuge/items/d997389529faffab33ba

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

[iOS] [Swift] ローカル通知とアプリのライフサイクルについて

はじめに

これはiOS アプリでプッシュ通知についての記事です。

これから学ぶ知識

  • 通知
  • AppDelegate / SceneDelegate
  • UNNotificationContent
  • UNNotificationTrigger
  • UNNotificationRequest

通知

概要

スマートフォンを使っていると 1 日に 1 度はみることのある通知は、ユーザーがアプリを開いていなくてもアプリに関する情報を伝えられるというメリットが強力です。

例えば、アラームアプリはユーザーがアプリを開いていなくてもその時刻が来たことを適切に伝えてくれます。

また、通知にはいくつかの種類がありますが、よく使われるのは以下のものです。

アラート(リスト) アラート(バナー)
List Banner
バッジ サウンド
Badge No Image

アプリケーションのライフサイクル

プログラム上では通知を受け取った際のアプリの状態によって処理が変わってきます。
状態というのはアプリが今起動しているかアプリが画面上に表示されているかといったもののことです。

そこで、通知と紐付けて、iOS アプリケーションにおけるライフサイクルを確認しましょう。
ライフサイクルとは生まれてから消えるまでの過程のことです。

フォアグラウンド(Foreground)

フォアグラウンドとはアプリケーションが画面上に表示されている状態のことを指します。

Active / In Active の 2 種類に細かく分けられます。

バックグラウンド(Background)

バックグランドとはアプリケーションが画面上に表示されていない状態のことを指します。

Backgroundの状態で一定時間起動が続くと、システムが自動的にSuspended(停止状態)に切り替えてくれます。

アプリケーションのライフサイクル
Lifecycle
種類 説明
Active アプリが現在操作中である 使用中
InActive アプリの操作から一時的に離れている状態であるが、アプリは表示されている コントロールセンター・通知センターを表示している状態
Foreground ActiveInActiveの状態の総称
Background 別のアプリケーションが表示されている状態。アプリは起動していてバックグランドで特定のタスクが可能
Suspended 一定時間のBackground状態が続くと自動的にこの状態になる
Not Running アプリが完全に停止している状態

細かく分けられるものの、アプリケーションにはフォアグラウンドバックグラウンドの状態があることが重要です。
SuspendedNot Runningに関してはシステムが自動で管理する部分もありあまり開発者でも意識することはありません。

AppDelegate について

AppDelegate とは@UIApplicationMain の属性をもつクラスで、アプリごとに 1 つのみ存在します。

SceneDelegateについて

SceneDelegateとはAppDelegateとは異なり、アプリごとに複数存在することがあります。iOS13から導入されたMultiple Windowsによって一つのアプリを複数のウィンドで操作できることがあるため、アプリケーションのライフサイクルはAppDelegateよりもSceneDelegateの方が優先されるようになりました。(UIApplicationDelegate#applicationDidBecomeActiveはSceneDelegateが設定されている場合は呼ばれません。)

例えば、アプリがActiveなのかそうでないのかを判断する場合は以下の2つのメソッドを実装する必要があります。

  • func sceneDidBecomeActive(_ scene: UIScene)
    • シーンのライフサイクルがアクティブになった際に呼ばれます。
  • func sceneWillResignActive(_ scene: UIScene)
    • シーンのライフサイクルがアクティブでなくなった際に呼ばれます。

許可について

通知はアプリをインストールしてくれた全てのユーザーに対して送ることができるわけではありません。

実際に通知を送るには下の画像の様に予めユーザーに許可をとる必要があります。

通知許可アラート
Request Notification Authorization

通知の許可を送るコード

以下のコードでアラート バッジ サウンドの3つに対して許可のリクエストをすることができます。

// プッシュ通知の許可を依頼する際のコード
UNUserNotificationCenter.current().requestAuthorization([.alert, .badge, .sound]) { (granted, error) in
    // [.alert, .badge, .sound]と指定されているので、「アラート、バッジ、サウンド」の3つに対しての許可をリクエストした
    if granted {
        // 「許可」が押された場合
    } else {
        // 「許可しない」が押された場合
    }
}

通知を送信する

UNMutableNotificationContent

通知を送信する際にUNMutableNotificationContentというクラスを作成します。

下の例は、その通知によって付与されるバッジの数・通知のタイトル・通知を受信した際のサウンドを決定しています。

// MARK: 通知の中身を設定
let content: UNMutableNotificationContent = UNMutableNotificationContent()
content.title = notificationTitle
content.sound = UNNotificationSound.default
content.badge = 1

UNNotificationTrigger

次にUNNotificationTriggerというクラスを作成します。
UNNotificationTriggerはトリガーという言葉が含まれていることから分かる様に、いつ通知を発行するのかを設定するためのものです。
実際には以下の4つのクラスを使用してトリガーを作成します。

  • UNCalendarNotificationTrigger
    • カレンダーを元にするトリガー
    • DateComponentsを用いて特定の時間帯になると通知が発行される
    • 次の朝9時
    • 毎朝9時
  • UNLocationNotificationTrigger
    • 位置を元にするトリガー
    • ある特定の地域に位置していると通知が発行される
  • UNTimeIntervalNotificationTrigger
    • 時間差を元にするトリガー
    • 60秒後
    • 1週間後
  • UNPushNotificationTrigger
    • リモート通知におけるトリガー

作成例

ある日付データnotificationDateに対して年/月/日/時間/分が同じ際に一回のみ通知される際のトリガーは以下のように書けます。

// MARK: 通知をいつ発動するかを設定
// カレンダークラスを作成
let calendar: Calendar = Calendar.current
let trigger: UNCalendarNotificationTrigger = UNCalendarNotificationTrigger(dateMatching: calendar.dateComponents([.year, .month, .day, .hour, .minute], from: notificationDate), repeats: false)

UNNotificationRequest

次にUNNotificationRequestというクラスを作成します。

UNNotificationRequestは先ほどのUNNotificationContentUNNotificationTriggerの二つを用いて作成することができます。

作成例

// MARK: 通知のリクエストを作成
let request: UNNotificationRequest = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

identifierにはUUID().uuidStringという一意の(重複して生成されることのない)文字列を使用します。通知ごとにユニークで他と被ることがない必要があります。

通知を登録(送信)する

最後に作成した通知リクエストを通知センターに登録して、通知を送信します。
コールバック引数の中で、実際にエラーが返ってきたかどうかをerror: Error?引数によって判断することができます。

// MARK: 通知のリクエストを実際に登録する
UNUserNotificationCenter.current().add(request) { (error: Error?) in
    // エラーが存在しているかをif文で確認している
    if error != nil {
        // MARK: エラーが存在しているので、エラー内容をprintする
    } else {
        // MARK: エラーがないので、うまく通知を追加できた        
    }
}

通知を受け取った際の処理を実装する

設定した時刻になり、ユーザーが通知を受信したときの処理をする必要があります。

UNUserNotificationCenterDelegateに準拠した上で、以下の 2 つのメソッドを実装する必要があります。

class ClassA: UNUserNotificationCenterDelegate {
    // フォアグラウンドの状態でプッシュ通知を受信した際に呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.banner, .list])
    }

    // バックグランドの状態でプッシュ通知を受信した際に呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}

通知を受け取った際に、アプリが画面に表示されている(フォアグラウンド)か、そうではない(バックグランド)かで、別のメソッドを実装する必要があることが分かると思います。

頭にあるClassA は例で、実際には以下のようにAppDelegateUNUserNotificationCenterDelegateを準拠して 2 つのメソッドを実装します。

extension AppDelegate: UNUserNotificationCenterDelegate {
    // フォアグラウンドの状態でプッシュ通知を受信した際に呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.banner, .list])
    }
    // バックグランドの状態でプッシュ通知を受信した際に呼ばれるメソッド
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}

具体的なサンプル

+ボタンから作成したプッシュ通知を一覧画面で確認できるアプリです

demo

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

RxSwift Transforming Operators

Transforming Operatorsまとめ

RxSwiftでコードを書く必要があり、学習中です。
参考文献を元に学んだ内容をすぐ振り返れるようにまとめていきます。
今回はTransforming Operatorsについてまとめました。
完全に自分の解釈でコメントを載せていますが、解釈に誤りがありましたら、コメント欄より教えてください。

toArray()

Observableから発火されたelementをArrayに詰め込んでSingleで返す。

import Foundation
import RxSwift

    let disposeBag = DisposeBag()
    // 1
    Observable.of("A", "B", "C")
        // 2
        .toArray()
        .subscribe(onSuccess: {
            print($0)
        })
        .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-24 at 3.16.47 PM.png

map()

標準と一緒。各値に処理を加えて返す。

    let disposeBag = DisposeBag()

    // 1
    let formatter = NumberFormatter()
    formatter.numberStyle = .spellOut

    // 2
    Observable<Int>.of(123, 4, 56)
        // 3
        .map {
            formatter.string(for: $0) ?? ""
        }
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-24 at 3.18.30 PM.png

enimerated()

Observableから受け取った値にindexを振り管理できる。

    let disposeBag = DisposeBag()

    // 1
    Observable.of(1, 2, 3, 4, 5, 6)
        // 2
        .enumerated()
        // 3
        .map { index, integer in
            index > 2 ? integer * 2 : integer
        }
        // 4
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-24 at 3.20.46 PM.png

compactMap()

map+nilを取り除くfilter

    let disposeBag = DisposeBag()

    // 1
    Observable.of("To", "be", nil, "or", "not", "to", "be", nil)
        // 2
        .compactMap { $0 }
        // 3
        .toArray()
        // 4
        .map { $0.joined(separator: " ") }
        // 5
        .subscribe(onSuccess: {
            print($0)
        })
        .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-24 at 3.22.18 PM.png

flatMap()

Observableが変わっても、継続して以前のObservableから値を受け取り続ける。

struct Student {
    let score: BehaviorSubject<Int>
}

    let disposeBag = DisposeBag()

    // 1
    let laura = Student(score: BehaviorSubject(value: 80))
    let charlotte = Student(score: BehaviorSubject(value: 90))

    // 2
    let student = PublishSubject<Student>()

    // 3
    student
        .flatMap {
            $0.score
        }
        // 4
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)

    student.onNext(laura)
    laura.score.onNext(85)
    student.onNext(charlotte)
    laura.score.onNext(95)
    charlotte.score.onNext(100)

コンパイル結果
Screen Shot 2020-11-24 at 3.22.40 PM.png

flatMapLatest()

Map + switchLatest: Observableが変わったら最新のObservableからのみ値を受け取り、前のObservableからは値を受け取らない。

    let disposeBag = DisposeBag()

    let laura = Student(score: BehaviorSubject(value: 80))
    let charlotte = Student(score: BehaviorSubject(value: 90))

    let student = PublishSubject<Student>()

    student
        .flatMapLatest {
            $0.score
        }
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)

    student.onNext(laura)
    laura.score.onNext(85)
    student.onNext(charlotte)

    // 1
    laura.score.onNext(95)
    charlotte.score.onNext(100)

コンパイル結果
Screen Shot 2020-11-24 at 3.22.55 PM.png

materialize() && dematerialize()

materialize()しない場合、エラーが起こったとき、そこでdisposeされてしまうが、materialize()することでelementをeventに変換し、.dematerialize()でelementに戻すことで、エラーが起こったObservableはUnsubscribeされるが、別のObservableは継続して使用することができる。

    // 1
    enum MyError: Error {
        case anError
    }

    let disposeBag = DisposeBag()

    // 2
    let laura = Student(score: BehaviorSubject(value: 80))
    let charlotte = Student(score: BehaviorSubject(value: 100))

    let student = BehaviorSubject(value: laura)

    // 1
    let studentScore = student
        .flatMapLatest {
            $0.score.materialize()
        }

    // 2
    studentScore
        // 1
        .filter {
            guard $0.error == nil else {
                print($0.error!)
                return false
            }

            return true
        }
        // 2
        .dematerialize()
        .subscribe(onNext: {
            print($0)
        })
        .disposed(by: disposeBag)


    // 3
    laura.score.onNext(85)

    laura.score.onError(MyError.anError)

    laura.score.onNext(90)

    // 4
    student.onNext(charlotte)

コンパイル結果
Screen Shot 2020-11-24 at 3.23.10 PM.png

参考文献

RxSwift: Reactive Programming with Swift
Chapter7 Transforming Operators Written by Scott Gardner
詳細が気になる方はこちらをご参照ください!
https://www.raywenderlich.com/13285844-rxswift-reactive-programming-with-swift-update-now-available

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

RxSwift FilterOperators チートシート

FilterOperators

RxSwiftでコードを書く必要があり、学習中です。
参考文献を元に学んだ内容をすぐ振り返れるようにまとめていきます。
今回はFilter Operatersについてまとめました。
完全に自分の解釈でコメントを載せていますが、解釈に誤りがありましたら、コメント欄より教えてください。

elementAt()

指定したインデックスの場合のみ、インベントは発火

import Foundation
import RxSwift
  // 1
  let strikes = PublishSubject<String>()
  let disposeBag = DisposeBag()

  //  2
  strikes
    .elementAt(2)
    .subscribe(onNext: { _ in
      print("You're out!")
    })
    .disposed(by: disposeBag)

    strikes.onNext("X")
    strikes.onNext("X")
    strikes.onNext("X")

コンパイル結果
Screen Shot 2020-11-23 at 12.26.48 AM.png

filter()

この場合、2で割り切れる値のみ表示する。条件に当てはまる場合のみ、イベントを発火。

  let disposeBag = DisposeBag()
  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .filter { $0.isMultiple(of: 2) }
    // 3
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.26.21 AM.png

Skip()

先頭か指定された数まで、イベント発火をスキップする。

  let disposeBag = DisposeBag()

  // 1
  Observable.of("A", "B", "C", "D", "E", "F")
    // 2
    .skip(3)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.25.57 AM.png

SkipWhile()

ある条件が成立するまで、イベントを通さない。一度通ったあとは、全部通す。

  // 1
  Observable.of(2, 2, 3, 4, 4)
    // 2
    .skipWhile { $0.isMultiple(of: 2) }
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.25.17 AM.png

SkipUntil()

動的にイベントを発火させる。トリガーが起動された後のイベントを全て発火する。

  let disposeBag = DisposeBag()

  // 1
  let subject = PublishSubject<String>()
  let trigger = PublishSubject<String>()

  // 2
  subject
    .skipUntil(trigger)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

    subject.onNext("A")
    subject.onNext("B")
    trigger.onNext("X")
    subject.onNext("C")
    subject.onNext("D")

コンパイル結果
Screen Shot 2020-11-23 at 12.24.51 AM.png

take()

Skipオべレーターの逆、指定した数までイベントを発火、それ以降はイベントを止める。

  let disposeBag = DisposeBag()

  // 1
  Observable.of(1, 2, 3, 4, 5, 6)
    // 2
    .take(3)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.24.20 AM.png

takeWhile()

条件が満たされる間は、イベントを発火

  let disposeBag = DisposeBag()

  // 1
  Observable.of(2, 2, 4, 4, 6, 6)
    // 2
    .enumerated()
    // 3
    .takeWhile { index, integer in
      // 4
      integer.isMultiple(of: 2) && index < 3
    }
    // 5
    .map(\.element)
    // 6
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.23.39 AM.png

takeUntil()

条件が満たされるまでイベントを発火。最期のイベントを発火するかどうか選択できる。

  // 1
  Observable.of(1, 2, 3, 4, 5)
    // 2
    .takeUntil(.exclusive) { $0.isMultiple(of: 4) }
    .subscribe(onNext: {
      print($0)
    })
  .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.23.05 AM.png

takeUntil() Trigger

トリガーが軌道するまで、イベントを発火。それ以降は発火しない。

  let disposeBag = DisposeBag()

  // 1
  let subject = PublishSubject<String>()
  let trigger = PublishSubject<String>()

  // 2
  subject
    .takeUntil(trigger)
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

  // 3
  subject.onNext("1")
  subject.onNext("2")
  trigger.onNext("X")
  subject.onNext("3")

コンパイル結果
Screen Shot 2020-11-23 at 12.21.41 AM.png

distinctUntilChanged()

エレメントが重複する場合、一度目のイベントのみを発火、それ以降はエレメントが変わるまでイベントを発火しない。

  // 1
  Observable.of("A", "A", "B", "B", "A")
    // 2
    .distinctUntilChanged()
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.20.28 AM.png

distinctUntilChanged() 条件付き

後の数字の文字列に同じ要素が含まれないか確認、含まれる場合はスキップ、含まれない場合はイベントを発火!

  // 1
  Observable.of("A", "A", "B", "B", "A")
    // 2
    .distinctUntilChanged()
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)
}

  let disposeBag = DisposeBag()

  // 1
  let formatter = NumberFormatter()
  formatter.numberStyle = .spellOut

  // 2: フォーマッターで変換しなくていいように、はじめからNSNumberに!!
  Observable<NSNumber>.of(10, 110, 20, 200, 210, 310)
    // 3
    .distinctUntilChanged { a, b in
      // 4 
      guard
        let aWords = formatter
          .string(from: a)?
          .components(separatedBy: " "),
        let bWords = formatter
          .string(from: b)?
          .components(separatedBy: " ")
        else {
          return false
      }

      var containsMatch = false
      // 5
      for aWord in aWords where bWords.contains(aWord) {
        print(aWord)
        containsMatch = true
        break
      }

      return containsMatch
    }
    // 6
    .subscribe(onNext: {
      print($0)
    })
    .disposed(by: disposeBag)

コンパイル結果
Screen Shot 2020-11-23 at 12.19.58 AM.png

Filter応用

import Foundation
import RxSwift

  let disposeBag = DisposeBag()

  let contacts = [
    "603-555-1212": "Florent",
    "212-555-1212": "Shai",
    "408-555-1212": "Marin",
    "617-555-1212": "Scott"
  ]

  func phoneNumber(from inputs: [Int]) -> String {
    var phone = inputs.map(String.init).joined()

    phone.insert("-", at: phone.index(
      phone.startIndex,
      offsetBy: 3)
    )

    phone.insert("-", at: phone.index(
      phone.startIndex,
      offsetBy: 7)
    )

    return phone
  }

  let input = PublishSubject<Int>()

  input
    .skipWhile {return $0 == 0}
    .filter {return  $0 < 10 }
    .take(10)
    .toArray()
    .subscribe(onSuccess: {
      let phone = phoneNumber(from: $0)
      if let contact = contacts[phone] {
        print("Dialing \(contact) (\(phone))...")
      } else {
        print("Contact not found")
      }
    })
    .disposed(by: disposeBag)

    //0 is rejected
  input.onNext(0)
    //Only figure less than 10 can be passed 
  input.onNext(603)

  input.onNext(6)
  input.onNext(0)
  input.onNext(3)

  "5551212".forEach {
    if let number = (Int("\($0)")) {
      input.onNext(number)
    }
  }

  input.onNext(9)

コンパイル結果
Screen Shot 2020-11-23 at 1.16.36 PM.png

参考文献

RxSwift: Reactive Programming with Swift
Filtering Operators Written by Scott Gardner
詳細が気になる方はこちらをご参照ください!
https://www.raywenderlich.com/13285844-rxswift-reactive-programming-with-swift-update-now-available

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

[Swift] APIレスポンス値はそのまま判定せずenumに置き換えた方が良い

前提環境:
・Xcode:12.1
・Swift:5.3

私はコードレビュー等でしばしば以下のような実装を見かけるのですが、Swiftの良さを活かしていないなと感じます…

改善前
// APIレスポンスの値と仮定
let memberType = "1"

func foo() {
    switch memberType {
    case "0":
        print("通常会員の処理を行う")
    case "1":
        print("プレミアム会員の処理を行う")
    case "2":
        print("お試し会員の処理を行う")
    default:
        break
    }
}
foo() // "プレミアム会員の処理を行う"

以下が改善したコードです。

改善後
// APIレスポンスの値と仮定
let memberTypeRaw = "1"

enum MemberType: String {
    case normal = "0"
    case premium = "1"
    case trial = "2"
}

func foo() {
    guard let memberType = MemberType(rawValue: memberTypeRaw) else {
        preconditionFailure("定義されていない値")
    }
    switch memberType {
    case .normal:
        print("通常会員の処理を行う")
    case .premium:
        print("プレミアム会員の処理を行う")
    case .trial:
        print("お試し会員の処理を行う")
    }
}
foo() // "プレミアム会員の処理を行う"

改善後のコードには以下の利点があります。

  • enumを定義することによって、APIレスポンスとして返却されうる値に対して型と名前が付与されることで、その仕様が読み取りやすくなる。
  • guard節によって、MemberType型に変換した結果がnilである=仕様として想定外の値である、という実装意図が明確である。
  • 将来enum MemberTypeにcaseが増えた際に、switch memberTypeにてcaseを網羅していないとコンパイルエラーとして検出でき、改修漏れを防止できる。

一行でまとめると、
Swiftの「Null安全」、「型安全」、そして「コンパイラによるenumの全ケース網羅チェック」という特徴を活かしましょう、
というお話でした。

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