ひとしれずひっそり

主にソフトに関することをメモしていきます。過程をそのまま書いていたりするので間違いが含まれます。鵜呑みしない様に。

Combine (3)

次はこちら

developer.apple.com

Combine framework

  • Combine frameworkはEventを宣言的なアプローチ(a declarative approach)で提供できる。
  • Delegateを使った複数のCallbackやHandlerを使用したClosure処理よりも、イベントを一つの処理chainとして作ることができ可能性(有用性?)がある。
  • Combine operatorで前の処理から引き継いで個別の処理をしていくことができる。

  • テーブルなどで検索する時に、AppKitでは検索文字を入れるたびに通知が行くがCombineを使用すると最終の検索結果の更新の仕方(内容やタイミング)をコントロールすることができる。

PublisherとSubscriberの接続

  • Publisherは発行する型をOutputで指定する。
  • Subscriberは受取る型をInputで指定する。
  • どちらも扱うErrorをFailureとして指定する。
  • PublisherとSubscriberを接続するにはInputとOutput、それぞれのFailureの型が一致していないといけない。
  • built-in subscribersを用いるとSubscriberのInputとFailuerを自動的にPublisherに一致させる。
  • sink()、assign()はSubscriberを生成する(built-in subscribersということか?)。

sink(receiveCompletion:receiveValue:)

  • sink()は2つのClosureを持つ
    • 一つはPublisherが終了したり異常で終了した際に Subscribers.Completion が発行された時の処理を入れる。
    • もう一つはPublisherから値を受取った時の処理を入れる。

assign(to:on:)

assign()はPublisherから受取った値を直ぐに、指定されたObjectのkey pathで指定されたPropertyに割当てる。

sink()を使った例

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .sink(receiveCompletion: { print ($0) },
          receiveValue: { print ($0) })
  • sink()、assign()どちらもPublisherに無制限に直ぐさま発行を要求する。
  • 発行周期をコントロールするには独自のSubscriber protocolを適用したSubscriberを準備する必要がある。

Playgroundで試してみる。ドキュメントのコードははmac os用のコードなのでiOS向けに変えている。

    var sub: AnyCancellable!

        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .sink(receiveCompletion: { print ($0) },
                  receiveValue: { print ($0) })

LearningCombine.playground · GitHub

Outputの型をOperatorで変える

  • 上の例ではNotificationオブジェクトが渡されてくる。
  • 最終段では文字列として扱う方が便利が良い。
  • Combineでは流れを変えるOperatorとしてmap(:)やflatMap(maxPublishers::)、reduce(::)が用意されている。
  • map() Operatorを使うと違う型を渡すことができる。
let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .assign(to: \MyViewModel.filterString, on: myViewModel)

Playgroundで試してみると、なるほどテキストで通知されている。 LearningCombine.playground · GitHub

assignを使うとpropertyに渡すことができる。

Playgroundでassign()でUILabel.textに渡してみる。
TextFieldで変更すると同じテキストがLabelにも反映される。
SwiftUIでやっていることに近いじゃないか。

        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .map( { ($0.object as! UITextField).text ?? "" } )
            .assign(to: \MyViewController.label.text, on: self)

LearningCombine.playground · GitHub

Operatorを使ったPublisherの拡張

Operatorを使ってPublisherを拡張できる。
3つの方法が挙げられている。

  • 一つはfilter(_:) で不適切な文字を弾いたり文字数制限するなどで使用する。
  • フィルター処理が高くつく(DBが大きいや問い合わせ回数が多いなど)場合にユーザーの入力が終わるのを待ってからEventを発行したい時には、 debounce(for:scheduler:options:)が使える。前回の発行からの次の発行までの最小時間を設定できるので間引くことができる。RunLoopクラスが時間を与える便利な機能を提供している。
  • UIを更新する場合にCallbackをmain threadで処理させたい時にreceive(on:options:) が使える。RunLoop クラスで提供されるSchedulerを最初の引数として渡すとmain loopで受け取ることができる様になる。
        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .map( { ($0.object as! UITextField).text ?? "" } )
            .filter( { $0!.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .receive(on: RunLoop.main)
            .assign(to: \MyViewController.label.text, on: self)

filter()で英数字のみ受け付ける様になる。
debounce()で500ms経つとEventを発行する様に遅延できる。
receive()でmain threadで受け取る様にできる。

LearningCombine.playground · GitHub

今まで色々試行錯誤してた事がPublisherで実現できるのではないかという気がしてきた。

Publishingのキャンセル

  • Publisherは正常終了するか異常で終了するまで発行し続ける。
  • SubscriberでSubscribeが不要になったらPublisherに伝えることでキャンセルできる。
  • Cancellable protocolでcancel()メソッドが提供され、Subscriberに適用する。cancel() でキャンセルする。
  • sink()、assign()ではCancellable protocolが適用されている。
  • PublisherはSubscriberが最初にSubscribeした時に Subscription オブジェクトを送る。
  • Custom Subscriberを作る場合はCancellableを適用する必要がある。
  • Custom SubscriberでPublisherから渡されたSubscriptionオブジェクトを保有し、cancel()で保有したSubscriptionにcancel()を呼び出す必要がある。

Combineの理解がだいぶ進んだ。

Combine (2)

ここを読んでいる。

developer.apple.com

Publisher

  • Publisherはprotocolである。
  • upstream publisherから値を受け取る。
  • それを発行する
  • Publisherはchainすることができて、間に入ったPublisherはデータを加工したりするのに使える。
  • 最終のPublisherからSubscriberは値を受け取る。
  • Publisherは値を発するのみで、そのタイミングはSubscriberが要求した時に発行される。
    • Subscriberが受け取るタイミングを調整できる。
  • Timer、NotificationCenter、URLSessionなどいくつかはPublisherが適用されている。
  • Key-Value Observingもbuilt-in publisherで提供されているのでpropertyはPublisherを通して扱える。

Timerは前回の理解通り適用されているが書かれている。
built-in publisherがどういうものかわからないけど@Publishedなどのキーワードで適用されるとかそんな感じだろうか?
Subscriberのタイミングで発行されるというのは理解していなかった。

SwiftUIは値が変更されたら発行して欲しいということだと思うが、俺れは毎回知りたい、とか私は10秒に1回でいいよ、とか僕は10個溜まったら知りたいとかできるということか?
今はよくわからんけど…

Combine (1)

SwiftUI で Timer を使用した時に Publisher というのがあって、TimerはPublisherという機能と関係があるんだ程度の認識だった。 なんとなく使えてるからいいかなとは思っていたが、理解できてないのはなんとなく気持ち悪い。

developer.apple.com

やっぱりよくわからないのだがCombineというフレームワークと関わっているらしい。

ここや、その参考としている先を読んでみるが、抽象的な感じでピンと来ていない。

qiita.com

zenn.dev

が、上の終わりに@Stateや@Bindingの理解というのがあって、これはSwiftUIでいつも目にしているものだ。
SwiftUIも理解しているわけではなくこうすればこうなる程度の理解で、突っ込んで学ぶいい機会かもしれない。

一応ここまでの感じでは今までDelegateやNotification、KVOでやっていた事がPublisherとSubscriberという関係でくくれる様になる。
SwiftUIはモデルの方がPublisherになって、Viewの方がSubscriberとなりデータが変更されるとViewに変更が伝えられて更新される。

あっているかどうかはこの先理解が進むと分かるだろう。

M5でmruby/c (3)

今までやったのを整理してQiitaにまとめた。

libディレクトリーに入れるやり方をしていたが、lib_depsで直接githubから読み込む様に変更している。
不要なファイルを除外するやり方が手作業なのが残念だ。

qiita.com

github.com

Background処理ではDispatchQueue.mainに気をつけろ

iOSアプリでバックグラウンドに移った時に区切りのいいところまで実行させたい時にバッググラウントでの処理を要求する必要がある。

こんな感じ

    private var taskIdentifier: UIBackgroundTaskIdentifier!

    func becomeBackground() {
        taskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
            // 与えられた時間内に終了しない場合の強制終了
            UIApplication.shared.endBackgroundTask(self.taskIdentifier)
        })

        // ここで区切りのいいところまで処理させる。
        
        // 終了したらバックグラウンドタスクを終了する
        UIApplication.shared.endBackgroundTask(taskIdentifier)
    }

例えば、カウンター処理でバックグラウンドに入ったらカウンターを停止させて終了したい。

カウンターはpublisherが処理していて停止指令をだしてから停止状態になるまで待たせる。

        // ここで区切りのいいところまで処理させる。
        publisher!.stop()
        while publisher!.stopped == false {
            Thread.sleep(forTimeInterval: 1)
        }

しかしpublisher!.stoppedはViewでも使用するのでDispatchQueue.main.async()を使用してmain threadで更新する様にしている。
BackgroundTask処理中はmain threadに処理が移らないため、stoppedにならずバックグラウンド処理が終了しない。

class Publisher: ObservableObject {
    
    @Published var run = false
    @Published var stopped = true
    @Published var count: UInt32 = 0
    
    private var thread: Thread!
        
    init() {
        thread = Thread(block: {
            DispatchQueue.main.async {
                self.stopped = !self.run
            }
            while(true) {
                while self.run {
                    Thread.sleep(forTimeInterval: 1)
                    DispatchQueue.main.async {
                        self.count += 1
                    }
                }
                DispatchQueue.main.async {
                    self.stopped = true
                }
                while !self.run {
                    Thread.sleep(forTimeInterval: 1)
                }
                DispatchQueue.main.async {
                    self.stopped = false
                }
            }
        })
        thread.start()
    }
    
    func start() {
        run = true
    }
    
    func stop() {
        run = false
    }
    
}

iOSのBackgrounTaskでデッドロック · GitHub

BackgroundTaskで扱う状態変数はPublishedにしない変数を使用しなければならない。
しかしView更新用のPublishedなのも欲しいので二重構造にする必要がある。

Publisher

    // view更新用
    @Published var stopped = true
    
    // 内部で扱う
    var stopped_rawValue = true {
        didSet {
            DispatchQueue.main.async {
                self.stopped = self.stopped_rawValue
            }
        }
    }
    
            while(true) {
                while self.run {
                    Thread.sleep(forTimeInterval: 1)
                    DispatchQueue.main.async {
                        self.count += 1
                    }
                }
                self.stopped_rawValue = true
                while !self.run {
                    Thread.sleep(forTimeInterval: 1)
                }
                self.stopped_rawValue = false
            }

Controller

        while publisher!.stopped_rawValue == false {
            Thread.sleep(forTimeInterval: 1)
        }

これでバックグラウンドに移ると一旦カウントが停止し、フォアグラウンドに戻ると再開できる様になる。

iOSのBackgrounTaskでデッドロック · GitHub

倒立振子に挑戦 (3)

倒立振子はToioを走らせてやろうとしていたが、通信で50〜110msくらいかかっている。
平均して10Hz位でしか指令を出せない事が分かった。
PID係数を調整してみるも倒立を保てそうにない。

手に持って竿燈の様にしても上手く保てないので、そもそも機構が悪い気がする。

それに加えてケーブルが抜けやすくM5AtomS3の電源が一瞬切れてしまう現象があって。
もうちょっとねばってダメならサーボモーター使う方法とかに変えないとダメかな…

同時並行処理(Concurrency) 難しい

ThreadとかDispatchQueueとかは経験あるけど非同期関数やらタスクは馴染みがない。

swift-programming-language-jp.gitbook.io

Actor

  • Actorはタスク間で情報を共有したいときに安全に情報を共有できる。
  • Actorの変更は1つのTaskからのみのため。

Sendable 型

  • Actorのメソッドに引数として渡す際やTaskの結果として返す時の安全性を持たせるのに使用する。

今の所どういう状況の時にSendable型が必要なのか分からず。