20200325のSwiftに関する記事は9件です。

【超初心者向け】クロージャの書き方【swift】

クロージャとは

swiftのLanguage Guideによると

クロージャは、コードの中で渡したり使用したりすることができる自己完結型の機能ブロックです。

中略

クロージャは、それらが定義されているコンテキストから任意の定数や変数への参照を取得して保存することができます。これは、それらの定数や変数の上で閉じることとして知られています。

最近話題のDeepLに翻訳してもらいました。

関数をクロージャにしてみよう!

以下の関数をクロージャで書いてみます。

・関数

func closurePractice(num1: Int, num2: Int) -> Int {
    return num1 + num2
}

・クロージャ

{ (num1: Int, num2: Int) -> Int in
    return num1 + num2
}

クロージャを使うと、このように処理を簡潔に記載することができます。

クロージャをもっと簡潔に書いてみる

型推論を使うことで、データ型の記載を省略することができます。

{ (num1, num2) in
    return num1 + num2
}

処理が1行の場合は、さらにreturnも省略することができます。

{ (num1, num2) in
    num1 + num2
}

参考サイト

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

RxDataSources × UITableView: アニメーション付きでセルを追加した際にスクロール位置を固定する

RxDataSourcesで困ってる人のために。
(もしかしたらもっと良い方法があるのかも)

追記:
Simulatorと実機とで挙動が違うっぽい。
今回の記事は実機でのみ有効です。

TL;DL

RxDataSources × UITableViewでスクロール位置を固定したい
→セルの更新処理がすべてRxDataSourcesに隠蔽されているのでスクロール位置を固定できない
→performBatchUpdatesをoverrideしよう

結論を先に

public class HogeHogeTableView: UITableView {
    private var onTop: Bool {
        return contentOffset.y == 0
    }

    public override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) {
        let bottomOffset = contentSize.height - contentOffset.y

        if !onTop {
            CATransaction.begin()
            CATransaction.setDisableActions(true)
        }
        super.performBatchUpdates(updates, completion: { finished in
            guard finished, !self.onTop else { completion?(finished); return }
            self.contentOffset = CGPoint(x: 0, y: self.contentSize.height - bottomOffset)
            completion?(finished)
            CATransaction.commit()
        })
    }
}

なぜスクロール位置がずれるのか?

TableViewに表示されている一番上のセルA(TableViewが保持している一番最初のセルであるとは限りません)のindexPath.rowをxだとすると、TableViewは何がなんでも、このxを保持しようとします。

このことを念頭に置き、実例に沿って考えてみましょう。

例えばTableViewの最上端にセルを一つinsertし、その直後にreloadしたとします。
するとreloadされた瞬間、すべてのセルのindexPathはそれぞれ+1ずつズレていきますから、セルAのindexPath.rowはx+1になる。

一方で、TableViewはxを保持しようとするので、表示されているセルがちょうど一つずつズレていく、そのようにしてスクロール位置がズレていくのです。

何が厄介なのか

厄介なのは以下2点です。

・セルの更新ロジックがすべてRxDataSources内部にかかれている
・アニメーション処理が絡んでくる

というわけでRxDataSourcesのソースから、reload/アニメーションに関するコードを探していきます。

RxDataSourcesのソースを読んでみる

どうやら、RxDataSources/RxTableViewSectionedAnimatedDataSource.swift#L100 あたりにセルの更新ロジックが記述されているようです。

よく見ると、performBatchUpdatesを使っていますね。
公式ドキュメントによると、

Animates multiple insert, delete, reload, and move operations as a group.

You can use this method in cases where you want to make multiple changes to the collection view in one single animated operation, as opposed to in several separate animations. You might use this method to insert, delete, reload, or move cells or use it to change the layout parameters associated with one or more cells.

複数のアニメーション処理をバッチとして実行するようなメソッドだと書かれています。

performBatchUpdates内でtableView.batchUpdatesを実行してますね。
試しにここを攻めていきましょう。

そこで以下のようにコードをいじってみたら、なぜかうまくいきました。

switch dataSource.decideViewTransition(dataSource, tableView, differences) {
case .animated:
    // each difference must be run in a separate 'performBatchUpdates', otherwise it crashes.
    // this is a limitation of Diff tool
    for difference in differences {
        let updateBlock = {
            // sections must be set within updateBlock in 'performBatchUpdates'
            dataSource.setSections(difference.finalSections)
            tableView.batchUpdates(difference, animationConfiguration: dataSource.animationConfiguration)
        }
        if #available(iOS 11, tvOS 11, *) {
            let bottomOffset = tableView.contentSize.height - tableView.contentOffset.y

            CATransaction.begin()
            CATransaction.setDisableActions(true)
            tableView.performBatchUpdates(updateBlock, completion: {
                guard $0 else { return }
                tableView.contentOffset = CGPoint(x:0,
                                                  y: tableView.contentSize.height - bottomOffset)
                CATransaction.commit()
            })
        } else {
            tableView.beginUpdates()
            updateBlock()
            tableView.endUpdates()
        }
    }

コードを直接いじるのもあれですし、他にperformBatchUpdatesを使う機会もなさそうなので適当にクラス作ってoverrideすれば終わりです。

public class HogeHogeTableView: UITableView {
    private var onTop: Bool {
        return contentOffset.y == 0
    }

    public override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) {
        let bottomOffset = contentSize.height - contentOffset.y

        if !onTop {
            CATransaction.begin()
            CATransaction.setDisableActions(true)
        }
        super.performBatchUpdates(updates, completion: { finished in
            guard finished, !self.onTop else { completion?(finished); return }
            self.contentOffset = CGPoint(x: 0, y: self.contentSize.height - bottomOffset)
            completion?(finished)
            CATransaction.commit()
        })
    }
}

なぜこれでうまくいくのか?

まず、performBatchUpdates完了時にcontentSize, contentOffsetなどの情報が正常に更新されるよう、CALayerのアニメーションを停止します。(参考)

  CATransaction.begin()
  CATransaction.setDisableActions(true)

performBatchUpdatesの処理が完了すると、TableViewにセルが追加されるので、contentSize.heightはセル分高くなりますから、どの程度の差分があるのかが気になりますね。

そこで、bottomOffsetを記憶します。

 let bottomOffset = contentSize.height - contentOffset.y

次にcontentOffsetをセル分ずらしてあげて、アニメーションを再開してあげれば完成です。

  self.contentOffset = CGPoint(x: 0, y: self.contentSize.height - bottomOffset)
  CATransaction.commit()

最後に、スクロール位置が最上端にある場合は、スクロール位置を固定しなくてもいいので、onTopで制約をつけます。

まとめ

performBatchUpdatesをoverrideしよう👍

public class HogeHogeTableView: UITableView {
    private var onTop: Bool {
        return contentOffset.y == 0
    }

    public override func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) {
        let bottomOffset = contentSize.height - contentOffset.y

        if !onTop {
            CATransaction.begin()
            CATransaction.setDisableActions(true)
        }
        super.performBatchUpdates(updates, completion: { finished in
            guard finished, !self.onTop else { completion?(finished); return }
            self.contentOffset = CGPoint(x: 0, y: self.contentSize.height - bottomOffset)
            completion?(finished)
            CATransaction.commit()
        })
    }
}

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

Swift 5.2の新機能

本日(2020年3月25日、現地時間では24日) Swift 5.2 がリリースされました

Swift 5.2 がフォーカスしているのは、コード補完やエラーメッセージの改善など、開発者の UX 改善で、言語仕様に加えられた変更は多くありません。 UX の改善点については公式ブログが詳しく解説しているので、本投稿では Swift 5.2 における言語仕様の変更点について紹介します。

Swift 5.2 で言語について加えられた変更は次の二つです。

SE-0249: Key Path Expressions as Functions

Key Path 式を関数として渡せるようにする変更です。

たとえば、 UserArray から ID の Array を作りたいときには、これまでは次のように書けました。

// BEFORE
let users: [User] = ...
let ids: [String] = users.map { $0.id }

Key Path 式を関数として渡せると、次のように書けるようになります。

// AFTER
let users: [User] = ...
let ids: [String] = users.map(\.id)

また、関数型の変数に Key Path 式を代入することもできるようになりました。

let getUserId: (User) -> (String) = \.id

しかし、これらは暗黙的変換であり、 Key Path 式自体が関数にように使えるわけではありません。

let id: String = \.id(user) // ⛔ NG

これを Key Path 式でやりたければ次のように書く必要があります。

let id: String = user[keyPath: \.id] // ✅ OK

なお、注意が必要なのは、 SE-0249 が対象としているのは Key Path 式だけで、 KeyPath インスタンスを関数型に暗黙的変換できるわけではないことです。たとえば、↓はできません。

let keyPath: KeyPath<User, String> = \.id
let ids: [String] = users.map(keyPath) // ⛔ NG

SE-0253: Callable values of user-defined nominal types

structclass などの nominal type を関数のように使えるようにする変更です。

たとえば、一次関数 $ax + b$ を表す型を作りたいとします。一次関数を関数ではなく nominal type として実装したい理由の例としては、 ab をプロパティで持つようにして、二直線の交点を求めるメソッドを作ったりしたいなどが考えられます1。しかし、一次関数型インスタンスは f(5) のように関数として使いたい気持ちにもなります。これまではそれらを両立できませんでした。

一次関数を Linear という型で表すとします。これまでは次のように書く必要がありました。

// BEFORE
let f = Linear(a: 2, b: 3) // 一次関数 2x + 3
print(f.evaluate(5))       // x = 5 を代入

Swift 5.2 からは、関数のように呼び出すことが可能になります。

// AFTER
let f = Linear(a: 2, b: 3) // 一次関数 2x + 3
print(f(5))                // x = 5 を代入

上記のように Linear を関数として使えるようにするには、 LenearcallAsFunction という名前のメソッドを実装します。

extension Linear {
    func callAsFunction(_ x: Double) -> Double {
        a * x + b
    }
}

callAsFunction メソッドを持つ型のインスタンスは何でも、関数のように呼び出せます。

しかし、 callAsFunction を持つインスタンスを関数型に暗黙的変換することはできないので注意が必要です。

let g: (Double) -> Double = f // ⛔ NG

関数型に変換するには明示的に callAsFunction メソッドを指定します。

let g: (Double) -> Double = f.callAsFunction // ✅ OK

なお、ここでは一次関数型を例に挙げましたが、 Proposal にはその他にも callAsFunction の使いどころが色々挙げられています

個人的に違和感を感じるところ

SE-0249 と SE-0253 は整合性のバランスが微妙に思えます。

関数のように使えない Key Path 式は関数型に暗黙的変換できるのに、

// 関数のように使う
//let id: String = \.id(user)            // ⛔ NG
let id: String = user[keyPath: \.id]     // ✅ OK

// 関数型へ変換
let getUserId: (User) -> (String) = \.id // ✅ OK

関数のように使える callAsFunction を持つインスタンスは関数型に暗黙的変換できません。

// 関数のように使う
print(f(5))                                  // ✅ OK

// 関数型へ変換
let g: (Double) -> Double = f                // ⛔ NG
let g: (Double) -> Double = f.callAsFunction // ✅ OK

SE-0253 の Future directions には Implicit conversions to function が書かれているので、 callAsFunction を持つインスタンスの暗黙的変換は将来的にサポートされるかもしれません。ただ、そうすると SE-0249 は KeyPathcallAsFunction を持たせるだけで良かったんじゃないかとも思います( Key Path 式だけでなく KeyPath インスタンスも関数型に暗黙的変換できるようになるのと、 KeyPath インスタンスを関数のように使えるようになるという違いがあるので、それらが望ましいかどうかの検討は必要ですが)。

まとめ

  • Swift 5.2 は開発者の UX 改善が中心で言語仕様の変更は少ない。
  • 言語仕様の変更に関する Proposal は SE-0249SE-0253 の二つ。
  • SE-0249 で Key Path 式が関数に暗黙的変換できるようになる。
    • 例: BEFORE users.map { $0.id } → AFTER users.map(\.id)
  • SE-0253 で callAsFunction メソッドを持つ型のインスタンスを関数のように使えるようになる。
    • 例: BEFORE f.evaluate(5) → AFTER f(5)
  • Swift 5.2 時点では、関数のように使えない Key Path 式は関数型に暗黙的変換できるのに、関数のように使える callAsFunction を持つインスタンスは関数型に暗黙的変換できないことに筆者は違和感を感じる。

  1. 実際に直線を表す型を作るときには、 $y$ 軸に平行な直線も表現できるように $ax + by + c = 0$ の形で実装することが一般的です。 

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

@Stateの値によって処理を制御する方法いくつか

はじめに

@State で定義した値に変化が生じた際、それに応じて処理を挟み込むいくつかの方法です。

準備

デモ用に、文字列の末尾が3の倍数だったらアホになるExtensionを生やします。

extension String {
    var needsAho: Bool {
        self.last
            .flatMap { Int("\($0)") }
            .flatMap { $0 % 3 == 0 } ?? false
    }
}

1. Computed Propertyを使う

  • 一番シンプルな方法で、値に変化があると即時に反映されます
struct ContentView: View {
    @State private var inputText: String = ""
    private var displayText: String {
        inputText.needsAho ? "🤪" : ""
    }

    var body: some View {
        VStack {
            TextField("title", text: $inputText)
            Text(displayText)
        }.padding()
    }
}

sample1.gif

2. Publishedを使う

  • 1.の方法では、例えば「1秒間の間に連続されて入力された場合、まとめて1回だけ処理を行いたい」といった場合などに対応できません
  • ObservableObjectPublished プロパティを生やすことで、Publisherとして扱うことができるようになるので、Combineオペレーターを使えるようになります
class ViewModel: ObservableObject {
    @Published var inputText: String = ""
    @Published var displayText: String = ""
    private var cancellables: Set<AnyCancellable> = []

    init() {
        $inputText
            .debounce(for: 1.0, scheduler: DispatchQueue.main)
            .map { $0.needsAho ? "🤪" : "" }
            .receive(on: DispatchQueue.main)
            .assign(to: \.displayText, on: self)
            .store(in: &self.cancellables)
    }

    deinit {
        self.cancellables.forEach { $0.cancel() }
    }
}

struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        VStack {
            TextField("title", text: $viewModel.inputText)
            Text(viewModel.displayText)
        }.padding()
    }
}

sample2.gif

3. CurrentValueSubjectを使う

  • 2の方法では、いちいち ObservableObject を作らなくてはなりません
  • CurrentValueSubject@State プロパティで持っておけば、Viewだけで同様の挙動を実現できます
struct ContentView: View {
    @State private var inputTextSubject: CurrentValueSubject<String, Never> = .init("")
    @State private var displayText: String = ""
    @State private var cancellables: Set<AnyCancellable> = []

    var body: some View {
        VStack {
            TextField("title", text: $inputTextSubject.value)
            Text(displayText)
        }
        .padding()
        .onAppear {
            self.inputTextSubject
                .debounce(for: 1.0, scheduler: DispatchQueue.main)
                .map { $0.needsAho ? "🤪" : "" }
                .receive(on: DispatchQueue.main)
                .assign(to: \.displayText, on: self)
                .store(in: &self.cancellables)
        }
        .onDisappear {
            self.cancellables.forEach { $0.cancel() }
        }
    }
}

sample3.gif

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

AVSampleBufferDisplayLayerを透過する

特殊なケースですが、透過情報を持ったSampleBufferをAVSampleBufferDisplayLayerにenqueueしても、背景が透過されないことがあります。

これはAVSampleBufferDisplayLayerが透過できない訳ではなく、enqueueしたCMSampleBufferに透過情報があるかどうかのフラグが有効になっていないため起こる問題です。

次のようにSampleBufferに含まれるimageBufferに対してkCVImageBufferAlphaChannelIsOpaqueフラグを落とせば透過が有効になります。

imageBuffer.attachments.merge([kCVImageBufferAlphaChannelIsOpaque as String : false], mode: .shouldPropagate)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】AppStoreアプリ申請 Reject(リジェクト)例

アプリ申請をしてみたら以下のようなリジェクトがあったので、共有ついでに、対処方法ご存知の方いましたら、おまちしています。

ちなみに今回が初めての申請で、、とはいえとてもしょうもないアプリをテスト的に配布してみようかなと思ったので、コンテンツ不足とかは予測していましたが、、、

リジェクト内容

Hello,

We are unable to continue this app’s review because your Apple Developer Program account is currently under investigation for not following the App Store Review Guidelines’ Developer Code of Conduct.

Common practices that may lead to an investigation include, but are not limited to:

- Inaccurately describing an app or service
- Misleading app content
- Engaging in inauthentic ratings and reviews manipulation
- Providing misleading customer support responses
- Providing misleading responses in Resolution Center
- Engaging in misleading purchasing or bait-and-switch schemes
- Engaging in other dishonest or fraudulent activity within or outside of the app

During our investigation, we will not review any apps you submit. Please do not create a new developer account or make any app transfers while waiting for the investigation to be completed. Once we have completed our investigation, we will notify you via Resolution Center. Due to the nature of the investigation you will be ineligible to receive an expedited review until the investigation is completed.

We do not require any additional information from you at this time, nor do we have any additional details to share. We appreciate your continued patience during our investigation.

Best regards,

App Store Review

ちなみにGoogle翻訳

こんにちは、

Apple Developer Programアカウントは現在、App Storeレビューガイドラインの開発者の行動規範に違反しているため、調査中であるため、このアプリのレビューを続行できません。

調査につながる可能性のある一般的な慣行には以下が含まれますが、これらに限定されません。

-アプリやサービスを不正確に説明している
-誤解を招くアプリコンテンツ
-信頼できない評価とレビュー操作に従事する
-誤解を招く顧客サポート応答を提供する
-解決センターで誤解を招く応答を提供する
-誤解を招く購入や餌とスイッチのスキームに従事
-アプリ内外での不正行為や不正行為

調査中、送信されたアプリは確認されません。調査が完了するまで、新しいデベロッパーアカウントを作成したり、アプリを転送したりしないでください。調査が完了すると、解決センターを通じて通知されます。調査の性質上、調査が完了するまで、迅速な審査を受けることはできません。

現時点では、お客様からの追加情報は必要ありません。また、共有する追加情報もありません。調査中、引き続きご理解いただきますようお願いいたします。

宜しくお願いします、

App Storeレビュー

参考までに

App Store Review Guidelines

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

[Swift5] Realm初心者が初心者に捧げるマニュアル [24/03/20]

Realm

モバイル向けデータベースでそれなりに早いのがウリらしい。暗号化もできたりして便利だなと思う一面、ややこしいところも多々あったので忘れないようにメモしておく。

この備忘録が誰かの助けになったのであれば嬉しい限りである。

自分が学ぶたびに付け足していくので頑張りたい所存。

データベースの構造

RealmSwiftをインポートしたら以下のように書ける。

import RealmSwift

class UserInfoRealm: Object {
    @objc dynamic var name: String = ""
    @objc dynamic var pid: String = ""
}

Optional Value

もしもNullを許容したい場合(Swiftではnilなのだが)は以下のように書くと良い。

class UserInfoRealm: Object {
    @objc dynamic var name: String? = nil
    @objc dynamic var pid: String? = nil
}

デフォルト値はnilでもいいし、何でも好きな値を代入しておけばいいと思う。

ただし、この書き方ができるのはString型だけでInt型では何故かできない。 つまり、以下のコードはコンパイルエラーが発生する。

class UserInfoRealm: Object {
    @objc dynamic var name: String? = nil
    @objc dynamic var age: Int? = nil // コンパイルエラーが発生する
}

どうすればいいのかというと以下のように書けばいい。

class UserInfoRealm: Object {
    @objc dynamic var name: String? = nil
    let age = RealmOptional<Int>()
}

なんでこんなややこしい仕様になっているのかはよくわからない。きっと偉い方の方針なんだろう。

リスト

リストというのは一つのレコードに対して複数のデータを保持する仕組みである。

ここでは、ある浮気症の人間のガールフレンドの名前をリストとして保持する場合を考えよう。

class UserInfoRealm: Object {
    @objc dynamic var name: String = ""
    dynamic var girlfriends = List<String>()
}

ここではなぜかObjective Cの宣言が不要になる。何故なのかはよくわかっていない。

しかし、名前だけでは不十分かもしれない。そのガールフレンドの素性まで知っておきたいというのが人間の性であろう。

ゴミコード

class UserInfoRealm: Object {
    @objc dynamic var name: String = ""
    dynamic var girlfriends_name = List<String>()
    dynamic var girlfriends_age = List<Int>()
}

しかしこんな感じでどんどんリストを追加してしまうのは良くない。なぜならこの構造だと名前と年齢に全く繋がりがないからだ。

例えばこの構造だと「年齢が30歳以下のガールフレンドの名前を全て列挙する」というクエリを書くのが死ぬほどめんどくさい。こんな構造を許してはいけないのだ。

じゃあどうすればいいのかというと、ガールフレンドの情報を登録するためだけの新たなクラスをつくり、クラスに対してリストを作ればいいのである。

// ユーザのクラス
class UserInfoRealm: Object {
    @objc dynamic var name: String = ""
    dynamic var girlfriends = List<GirlFriendsRealm>()
}

// ガールフレンドのクラス
class GirlFriendsRealm: Object {
    @objc dynamic var name: String = ""
    @objc dynamic var age: Int = 0
}

こうすればスッキリして非常に便利になる。ただ、このコードのままだと年齢がわからないガールフレンドの情報をレコードとして書き込んでしまうと0歳になってしまう。それを避けるためにも年齢はnilにしておいたほうがいいかもしれない。

Optionalのリスト

先程は必ず何らかの値が入っていることが保証されている(nilではない)リストを作成したが、当然Optional型のリストも作成したい場合があるだろう。

が、これはRealmの仕様上できないのだ。残念。

プライマリキーを設定する

class UserInfoRealm: Object {
    @objc dynamic var name = ""
    @objc dynamic var pid  = ""
    override static func primaryKey() -> String? {
        return "pid"
    }
}

名前が同じ人はいるかも知れないが、IDが同じ人は絶対にいないので固有の値としてpidを設定する。

他のデータベースではどうか知らないが、Realmのプライマリキーは以下の性質を持つ。

  • プライマリキーが同じ値を持つようなレコードは書き込めない
  • 一度設定したプライマリキーは上書きできない
  • プライマリキーを複数設定できない(当たり前だが)
  • 二つ以上のフィールドに対してプライマリキーが設定できない

特に最後の性質というか仕様がやっかいで、RealmにはMySQLなどにあるようなユニークインデックスのような仕組みがないので例えば「誕生日と名前が同じ人を複数登録できない」といった仕様がつくれない。

かといって誕生日にプライマリキーを張ってしまうと最大で366人しか登録できないし、名前に張ると同名の人が記録できないという問題が生じる。

これを解決する代替的な手法が複合キーと呼ばれる仕組みで、以下で解説する。

複合キー

ユニークインデックスを大変に不便にしたような仕組み。

class UserInfoRealm: Object {
    @objc dynamic var name = ""
    @objc dynamic var birth = ""
    @objc dynamic var key = ""

    func configure(name: String, birth: String) {
        self.key = name + birth
    }
    override static func primaryKey() -> String? {
        return "key"
    }
}

これはconfigure()というメンバ変数を定義し、その中で引数から新たなキーを生成してそれをプライマリキーにすることで擬似的なユニークインデックスを生成する仕組みである。

正直、めっちゃダサいと思うのだがこれ以外に解決する手段を検索で見つけることができなかった。知っている方がいたらぜひ教えてほしい。

だいたい、こんな書き方をしたらkeyに対してクエリを書くのがめんどくさいし、余計なフィールドが一つできるのも大変に気味が悪い。なにかうまい方法があったら教えて下さい、お願いします。

データベースの呼び出し

Realmの公式ドキュメントでは以下のような呼び出し方のサンプルコードが書かれているが、個人的にはオススメしない。

import RealmSwift

// realmにnilが代入されると、次の行でエラーが発生しクラッシュする
let realm = try! Realm()
let users = realm.objects(UserInfoRealm.self)

というのも、 Realm()はエラーを投げることができるクラスであるにも関わらずtry! Realm()とすることで変な値(主にnil)が返ってきたらエラー処理をする間もなくアプリがクラッシュしてしまうからだ。

import RealmSwift

// realmにnilは代入されるかもしれないが、次の行でエラー発生時にcatch内の処理に
// 移るのでクラッシュしない
do {
    let realm = try Realm()
    let users = realm.objects(UserInfoRealm.self)
} catch {
    // エラー処理
}

こうすることでdo内で一旦「エラーが起きるかもしれない処理」を実行し、エラーが発生した場合にはcatch内に移動することで柔軟に対応することができる。

ただこれは、特にエラー処理は必要ないなあって思っている人には不便かもしれない。

import RealmSwift

// realmにnilが代入されたらreturnするのでアプリがクラッシュしない
guard let realm = try? Realm() else { return }
let users = realm.objects(UserInfoRealm.self)

そういう方におすすめなのが上のコードでguard文とtry?の複合でエラーを柔軟に扱うことができる。

データの読み込み

今回は単純に保存されているユーザの情報を読み込む処理を考える。これは実は簡単でサンプルコードの四行目で一発で全レコードを取得することができるのだ。

ただ、Realmはモバイルでも動く動作の速さが一番のポイントであり、実際に四行目で全てのレコードが読み込まれているわけではないらしい。

これは単なるオブジェクト(インスタンス?プロパティ?)であり、実際にレコードにアクセスしたときにのみ、データを読み込んでいるらしいとかなんとか。

import RealmSwift

guard let realm = try? Realm() else { return }
let users = realm.objects(UserInfoRealm.self) // UserInfoRealmクラスのレコードを全取得

for user in users {
    debugPrint(user.name) // ユーザの名前を表示
}

取得したレコードは配列として扱えるので、for文で回すことができる!つまり、全ユーザの名前を表示するようなコードは上のように書けるわけである。

データの書き込み

データの書き込みには大雑把に「新規レコード」「既存レコードの値の変更」の二つがある。
どちらもちゃんとサンプルコードを載せるので参考にしていただきたい。

ちなみに書き込みにはdo-catch文を使うことが公式ドキュメントで推奨されている。じゃあなんで読み込み時も推奨しなかったんだろ、と思わなくもない。

import RealmSwift

guard let realm = try? Realm() else { return }
let users = realm.objects(UserInfoRealm.self) // UserInfoRealmクラスのレコードを全取得

do {
    try realm.write {
    // この中でしか値を変更するようなコードは書けない!
    }
} catch {
    // 書き込み失敗時のエラー処理
}

新規レコードの追加

単純にレコードを追加するだけなら以下のようなコードで実装できる。

import RealmSwift

guard let realm = try? Realm() else { return }
let user = UserInfoRealm() // インスタンスを作成

// インスタンスに値を設定する
user.name = "tkgling"

do {
    try realm.write {
        realm.add(user)
    }
} catch {
    // 書き込み失敗時のエラー処理
}

追加したいレコードがプライマリキーを含む場合は、プライマリキーが被った場合の処理を指定することができる。

import RealmSwift

guard let realm = try? Realm() else { return }
let user = UserInfoRealm() // インスタンスを作成

// インスタンスに値を設定する
user.name = "tkgling"
user.pid = "946312668650004480" // プライマリキー

do {
    try realm.write {
        realm.add(user, update: true)
    }
} catch {
    // 書き込み失敗時のエラー処理
}

このアップデート処理はプライマリキーがないテーブルに対しては行えないことに注意。trueとか.modifiedとか.allとかいろいろ指定できるけど何が違うのかいまいちわかっていない。

とりあえずわかっているのは同じプライマリキーの値がなければレコードを追加し、あれば値を更新するということくらいである。ただ、単純なこの実装では変更したくない値があるときに多少困ったことになる。

例えば以下のように名前・ID・年齢の三つの情報を持ち、IDをプライマリキーとするデータベース構造があったとし、指定したIDのユーザが名前を変更したのでその情報をデータベースに反映させたいとする。

class UserInfoRealm: Object {
    @objc dynamic var name: String = ""
    @objc dynamic var pid: String = ""
    let age = RealmOptional<Int>()
    override static func primaryKey() -> String? {
        return "pid"
    }
}

// こういう処理をするとどうなるか
guard let realm = try? Realm() else { return }
let user = UserInfoRealm() // インスタンスを作成

// インスタンスに値を設定する
user.name = "tkgling"
user.pid = "946312668650004480" // プライマリキー
do {
    try realm.write {
        realm.add(user, update: true)
    }
} catch {
    // 書き込み失敗時のエラー処理
}

するとこれは名前は確かに変更されるのだが、UserInfoRealm()のインスタンスを生成した段階で年齢としてnilが代入されているために名前だけを変更したいにもかかわらず年齢までもがnilに上書きされてしまうのだ。

これを防ぐためには、以下のようにアップデートしたい情報だけを記述する必要がある。

do {
    try realm.write {
        realm.add(UserInfoRealm.self, value: ["name": "tkgling", "pid": "946312668650004480"], update: true)
    }
} catch {
    // 書き込み失敗時のエラー処理
}

こうすればちゃんと名前だけが変更される。

マイグレーション

データベースの新たなフィールド(レコードではない!)を追加したときにスキームバージョンを上げて現在使っているデータベース構造が以前まで使っていたものとは違うことを明示する必要がある。

let config = Realm.Configuration(
    schemaVersion: 0,
    migrationBlock: { migration, oldSchemaVersion in
        migration.enumerateObjects(ofType: UserInfoRealm.className()) { old, new in
            // マイグレーションのコード
            }
    })
Realm.Configuration.defaultConfiguration = config

どこで呼び出すかは難しいところなのだが、まあ適当にアプリが起動したときに呼び出せばいいと思うのでAppDelegate.swiftにでも書いておくのが無難ではないだろうか。

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

Closureと[weak self]を理解する

はじめに

RxSwift便利ですよね。自称何でもリアクティブに書きたい芸人です。ただ、RxSwiftを学ぶときに[weak self]で痛い思いをしたのは私だけではないはず…!!Qiitaや他のブログとかでこの[weak self]の話を見かけることが時々あり、その度に「あるあるw」と思っています。

しかし、その解説でなんだか結論が
「Closure内では[weak self]をつけましょう」
と言ってる記事が多すぎる気がしました。

こういう記事を見かける度にいつも、「うーん、でもforEachとかの引数もClosureだよなー…。あれもつけるべきなのかなー」と考えてしまって悩んでいたので、その辺を調べたものをまとめます。

やってみた

forEachのClosure内で[weak self]を試しみました。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        array.forEach({[weak self] str in
            self?.string = str
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

案の定ですが、このViewControllerはpopViewControllerされると破棄されました。では、[weak self]を取り除いてみます。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        array.forEach({ str in
            self.string = str
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

このViewControllerをpopViewControllerしてみたところ、なんと破棄されていました!

「うーん、、、???」となっていましたが、別のClosureを試してみるとなんとなく納得できる結果を得られました。それは、DispatchQueue.main.asyncAfterです。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
            self.string = "aaa"
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

このViewControllerに遷移し1秒くらいでpopViewControllerしたところ、少し経ってから破棄されるのが確認されました(たぶんpopから9秒後)。また、これに[weak self]をつけるのも試してみました。

import UIKit

class ViewController: UIViewController {
    let array = ["aaa", "bbb", "ccc"]
    var string = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {[weak self] in
            self?.string = "aaa"
        })
    }

    deinit {
        print("破棄されたよー")
    }
}

結果は、popViewController時にすぐに破棄されていました。
(※ついでに[weak self]ではなく[unowned self]を試すと、10秒後にfatal errorが発生しました。)

わかったこと

標準で提供されているClosureを引数にとる関数は、その関数が必要なくなったタイミングで破棄されているのだと思いました。forEachは、その処理が終わったタイミングでClosureが破棄されClosure→Selfへの参照がなくなっていたため、popViewControllerのときに参照カウントが0になり破棄されたんだと思います。asyncAfterは、popViewController時はClosureがまだ生きておりClosure→selfの参照が残っていたのでViewControllerが破棄されず、Closureが実行され処理が終わったタイミングでClosureが破棄されselfの参照カウントが0になったので破棄されたんだと思います。

つまり、標準提供されている関数はClosure実行終了時にClosureが破棄され循環参照が解除されるので、[weak self]を付ける必要がないことがわかりました。

RxSwiftではなぜ[weak self]をつけるのか

ここまで調べてまた一つ疑問が出ました。
「RxSwiftでもClosure実行されたら破棄すればよくない?」
そう思ってRxSwiftのコードを追ってみましたが、明確な理由はわかりませんでした(難しすぎた…orz)。
ただおそらく以下のような理由だと考えています。RxSwiftは任意のタイミングでイベントを発行できます。ですので、何度も同じ処理を作っては破棄して作っては破棄して、を繰り返すよりは一つのインスタンス的な扱いで保持しておくほうがパフォーマンスが良かったのかと思います。その代わり、もう処理をしなくなったタイミングで破棄するために明確に宣言する必要がでてきます。それがRxSwiftのdisposeメソッドだと考えました。そのため、以下の実装を試してみました。

import UIKit
import RxSwift

class ViewController: UIViewController {
    let publishSubject = PublishSubject<String>()
    var eventString = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        publishSubject.subscribe(onNext: { str in
            self.selfString = str
        })
        publishSubject.dispose()
    }

    deinit {
        print("破棄されたよー")
    }
}

RxSwiftでは有名な、Closure内の[weak self]をつけ忘れているコードです。ですが、このViewControllerをpopViewControllerしてみると、正しく破棄されたのが確認されました!つまり、dispose()メソッドを呼べばClosureが破棄され循環参照が解除されたため、破棄できるようになったということがわかりました。

import UIKit
import RxSwift

class ViewController: UIViewController {
    let publishSubject = PublishSubject<String>()
    var eventString = ""
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        publishSubject.subscribe(onNext: { str in
            self.selfString = str
        }).disposed(by: disposeBag)
    }

    deinit {
        print("破棄されたよー")
    }
}

よく見かける上記のコードですが、これはdisposeBagが破棄されたタイミングでdisposeメソッドを呼ぶように書かれています。しかしselfが破棄されないとdisposeBagも破棄されないため、永遠にdisposeメソッドは呼ばれず循環参照が続くコードになっていました。

まとめ

  • 標準提供されている関数では、Closureは処理が終わったタイミングで破棄されるため[weak self]をする必要がない
  • RxSwiftでは、Closureは処理が終わっても破棄されないため[weak self]で循環参照を防いでおく必要がある
  • RxSwiftではClosureを破棄する手段としてdispose()が提供されているため、それを正しく呼べれば手動で循環参照を解除することもできる

RxSwift内のClosureだけ気をつけておけばとりあえずは良さそうで、安心しました。

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

[Swift]デリゲートを使用しデータをViewControllerA→B、ViewControllerB→Aに渡す

1.はじめに

初投稿です。
デリゲートとプロトコルを使用して、ViewControllerB→Aに値を渡し処理するコードは以前から使っていましたが、その逆のViewControllerA→Bに渡せないかと思い試してみました。

2.所感

もしかしたら、使い所があるのかもしれませんが、デリゲートとプロトコルを使用してViewControllerA→Bに値を渡すのは面倒なので、シンプルにprepare(for segue)等で値を渡した方が楽に感じました。

3.コード(ViewControllerA)*動作確認済

import UIKit


//⑤(PassData Forward)プロトコルとデリゲートを使用し、ViewControllerからデータを取得し、SecondViewControllerで処理する。
protocol DataProvider: class {
    func provideData() -> String?
}

//②(PassData Forward) プロトコルDataProviderの通知先を通知先をSelfに指定。
protocol DataHandler: class {
    //prepforsegue時にproviderDelegate = selfがセットされる。
    var delegateForward: DataProvider? { get set }
}

class ViewController: UIViewController, UITextFieldDelegate {


    @IBOutlet weak var textField: UITextField!
    var name: String = ""


    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
    }

    @IBAction func button(_ sender: Any) {
        performSegue(withIdentifier: "goNext", sender: nil)

    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

        //*ココでViewController内のプロトコルDataProviderの処理を実施すると、まだSecondViewが生成されていないため、nilとなる。
        //①(PassData Forward) よって、Delegateの通知先をプロトコルDataHandlerを通じてSecondViewControllerに伝える。
        if let destination = segue.destination as? DataHandler {
            destination.delegateForward = self
        }
        //①(PassData BackWard)Delegateの通知先をSecondViewに伝える。
        if let destination = segue.destination as? SecondViewController{
            destination.delegateBackward = self
        }
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}


//⑥(PassData Forward)
//プロトコルとデリゲートを使用し、SecondViewControllerでDelegateが呼ばれた際に以下の処理をし、SecondViewに値を渡す。
extension ViewController:  DataProvider {
    func provideData() -> String? {
        return textField.text
    }
}

//④(PassData BackWard)
//プロトコルとデリゲートを使用し、SecondViewControllerからデータを取得し、ViewControllerで処理する。
extension ViewController: provideDataBackward{
    func passData(text: String?) {
        textField.text = text
    }


}


3.コード(ViewControllerB)

import UIKit

protocol provideDataBackward {
    func passData(text:String?)
}



class SecondViewController: UIViewController, UITextFieldDelegate, DataHandler{

    //③(PassData Forward)プロトコルDataHandlerを通じてViewControllerで設定されたプロトコルDataProviderのDelegate通知先を記憶している。
    weak var delegateForward: DataProvider?

    //③Segue時にViewControllerで設定されたDelegateの通知先(ViewController = self)を記憶している。
    var delegateBackward:provideDataBackward?

    @IBOutlet weak var textField: UITextField!{
        didSet{
            //④(PassData Forward) ViewControllerのプロトコルDataProviderのメソッドprovideData()から値を取り出す。
            //⑦(PassData Forward) 取り出した値をテキストフィールドに表示⑦
            if let data = delegateForward?.provideData(){
                textField.text = data
            }
        }
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = self
        // Do any additional setup after loading the view.

    }

    @IBAction func button(_ sender: Any) {
        navigationController?.popToRootViewController(animated: true)
        // ②(PassData Backward) ViewController内でdelegateBackwardに設定された通知先(ViewController = self)にdelegateが実施されたことを伝え、データを渡す。
        delegateBackward?.passData(text: textField.text)
    }


    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }  
}

4.引用
Stack Overflow
Why passing data with delegate fails in Swift 4

https://stackoverflow.com/questions/47391115/why-passing-data-with-delegate-fails-in-swift-4

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