- 投稿日:2019-10-24T21:06:54+09:00
「LLVM bitcode はポータブルでない」ってどういう意味なの?
発端
LLVM bitcode はポータブルでない
http://shinh.skr.jp/slide/elvm/002.htmlこれがどういう意味なのか分からなくてずっと気になってました。
LLVMのbitcodeってターゲット非依存なんじゃないの?みたいな感じで。ここでは「ポータブルでない」というのを
(実行される)プラットフォーム
または(コンパイルの)ターゲット
に
依存している(dependent)
または固有のもの(specific)を含む
こと
ぐらいにふんわり理解してます。
「だから〇〇ができない」みたいなことは考えません。結論あるいは感想
- LLVM IRとそれをまとめたbitcode自体はターゲットに依存しない
- 実際にはだいたい(?)ターゲットに依存したIRが出力される
- C系の場合にはほぼ不可避
- Swiftでもターゲット依存のIRっぽい
- low levelな中間表現なんだからそりゃターゲット依存にもなるでしょ
- 正しいのかは分からないけどそういう気持ちになった
- ただしLLVMは何の略でもないことになってる
C/C++/Objective-C
Cとその上位互換たちの話のはずなので、Objective-Cもたぶん該当する
LLVM公式のFAQを翻訳してちょっとコメント
http://llvm.org/docs/FAQ.html#can-i-compile-c-or-c-code-to-platform-independent-llvm-bitcode
Question
Can I compile C or C++ code to platform-independent LLVM bitcode?
CやC++のコードをプラットフォームに依存しないLLVM bitcodeにコンパイルできますか?
Answer
3分割してタイトルを付けます。
コンパイル前(プリプロセッサ)
No. C and C++ are inherently platform-dependent languages. The most obvious example of this is the preprocessor. A very common way that C code is made portable is by using the preprocessor to include platform-specific code. In practice, information about other platforms is lost after preprocessing, so the result is inherently dependent on the platform that the preprocessing was targeting.
できません。CやC++は本質的にプラットフォーム依存な言語です。いちばん分かりやすい例としては、プリプロセッサがあります。Cのコードをポータブルにするためによくあるやり方は、プリプロセッサを使ってプラットフォーム固有のコードを含めることです。実際のところ、他のプラットフォームの情報はプリプロセスの後では失われ、その成果物はプリプロセスが対象としたプラットフォームに本質的に依存したものとなります。
要するに、LLVM IRを作るコンパイル以前に、既にプラットフォーム依存なコードになってるのが普通というわけです。
コンパイル時(sizeof)
Another example is sizeof. It’s common for sizeof(long) to vary between platforms. In most C front-ends, sizeof is expanded to a constant immediately, thus hard-wiring a platform-specific detail.
もう1つの例として、sizeofがあります。よく知られているように、sizeof(long)はプラットフォームごとに変わります。多くのC言語のフロントエンドでは、sizeofを定数値に直接展開して、プラットフォーム固有の詳細を埋め込みます。
ここでのフロントエンドは、clangとか、LLVMのフロントエンドのことですね。プリプロセッサのようにコンパイル前だけでなく、コンパイル過程でもプラットフォーム固有の値がそのまま入ってしまうのが普通、ということでしょう。
ABI
Also, since many platforms define their ABIs in terms of C, and since LLVM is lower-level than C, front-ends currently must emit platform-specific IR in order to have the result conform to the platform ABI.
また、多くのプラットフォームでABIはCレベルで定義され、そしてLLVMはCより低レベルなので、そのプラットフォームのABIに合わせた成果物を得るために、フロントエンドはプラットフォーム固有のIRを出力しなければならないのが現状です。
私はABIというものをちゃんと分かっていません。なので、それっぽい翻訳だけにとどめておきます。
Swift
SILのドキュメントでのLLVM IRの扱い
https://github.com/apple/swift/blob/master/docs/SIL.rst#sil-in-the-swift-compiler
SILというのは、コンパイル過程でASTとLLVM IRの中間にあるやつで、raw SILとcanonical SILの2段階があります。それ以外のことは何も分かってませんが、LLVM IRとの対比で興味深い表現を見つけました。
In contrast to LLVM IR, SIL is a generally target-independent format representation that can be used for code distribution, but it can also express target-specific concepts as well as LLVM can.
LLVM IRとは対照的に、SILは専らターゲットに依存しないフォーマットの表現形態で、コード配布に使うことができます。しかし、LLVMと同様に、ターゲット特有の概念を表現することもできます。
ここではLLVM IRがターゲット依存なものとして扱われているように読めます。これがC系のように不可避の現実なのか、(例えば何らかの最適化のための?)選択なのかは分かっていませんが、high-levelなSILではターゲットに依存しない表現を扱い、ターゲット依存なものはlow levelなLLVM IRの担当になっているようです。
Rust
LLVMのフロントエンドとして、C系、Swiftの他にRustでの扱いも挙げておきます。なお、私はRustのことは何も知りません。
Rustコミュニティでの質問と回答
- Cross Platform: Rust to LLVM IR and is LLVM IR is portable?
- 私の気になってた話と回答がそのままここに
- Rust特有の話っぽいのはあんまりちゃんと読んでません
- LLVM公式のFAQへのリンクもあります
TODO
MIRの記事から何か分かるといいな
- Introducing MIR
- SwiftのSILのように、LLVM IRの手前に別の中間表現があるようです。
- ほぼ図しか見てないので、そのうち読んで何か追記します。
- 投稿日:2019-10-24T15:46:23+09:00
[iOS13]UITextViewDelegateがTouchDownで反応してしまう
何が問題か
iOS13からテキストのリンクなどに用いられるUITextViewDelegateのtextViewメソッドが
iOS12まではTouchUp
で反応していたもののiOS13からなぜかTouchDown
で反応してしまうという問題がありました.
参考記事どう解決するか
- iOS13の不具合(?)の修正を待つ
- textViewメソッドのtextViewインスタンスからタッチイベントを取得する
- そもそもtextViewDelegateメソッドを使用しない
以上の3つがあると思います.
iOS13の不具合修正を待つ
いつAppleが修正を行うのかは不明瞭で今の所この修正はベータ版にも含まれていないようなので,これは懸命な解決法ではなさそうです.
textViewメソッドの
textView
インスタンスからタッチイベントを取得する以下のtextViewメソッドから受け取れるtextViewのタッチイベントを取得する方法です.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool以下のようにまず
interaction
で処理をわけ,textView
のタップイベント処理が終わった時に何かの処理をさせることで擬似的にTouchUp
の挙動と同じになるようにしています.出典はstackOverflowです.func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { switch interaction { case .invokeDefaultAction: if textView.gestureRecognizers?.contains(where: {$0.isKind(of: UITapGestureRecognizer.self) && $0.state == .ended}) == true { // Handle your custom logic here. return false } return true case .presentActions: // Default action. return true case .preview: // Default action. return true @unknown default: fatalError() } }そもそもUITextViewDelegateを使用しない
そもそもiOS13側の不具合である可能性が高く,いつ対応がされるのかがわからないため上記の
textView
のタッチイベントを拾ってtouchUp
のように振る舞わさせるのは少し冗長な気もします.
iOS13からURLなどを長押しした際のOS側の挙動が変わっている(以前までのアクションシートが起動するだけのものから変更されてプレビューが表示されるなどOS準拠の挙動がリッチになってきている)こともあり,あえて自分でそのさきを処理せず,OS準拠の挙動に合わせるということもありなのではないかなと思います.参考資料
- 投稿日:2019-10-24T15:38:01+09:00
iOSの有名リポジトリのコードの小技
うまい人の研究してわかったこと
- アクセス修飾子をちゃんと書く
- アクセス修飾子ごとにextentionを切る
internal var heroIDToSourceView = [String: UIView]()
というふうに、propertyをoptionalにするようなことはしない
- optinalはoptinalにする必要にするときだけ
- delegateはweakにする
UIView(frame: transitionContainer?.bounds ?? .zero)
nullのcheckに演算子を使うfor v in animatingToViews { _ = context.snapshotView(for: v) }
- 1行for文もあり
deferの使い方が上手い(loadingを消したり、初期化処理をしたりする)
- deferとは、途中でreturnしても実行される文
funcに対するコメントを書く
/** Update the progress for the interactive transition. - Parameters: - progress: the current progress, must be between 0...1 */
- propertyのViewControllerの宣言が、くっそうまい(関数的で、他に影響がないように書いている)
lazy var mmPlayerLayer: MMPlayerLayer = { let l = MMPlayerLayer() l.cacheType = .memory(count: 5) l.coverFitType = .fitToPlayerView l.videoGravity = AVLayerVideoGravity.resizeAspectFill l.replace(cover: CoverA.instantiateFromNib()) return l }()Layerのアニメーションに関して
fileprivate func setFrameWith(quadrant: VideoPositionType, dismissVideo: Bool) { let margin = self.config.margin var rect = self.config.playLayer?.playView?.frame ?? .zero let size = UIScreen.main.bounds switch quadrant { case .leftTop: rect.origin.x = dismissVideo ? -rect.size.width : margin rect.origin.y = margin case .rightTop: rect.origin.x = dismissVideo ? size.width : size.width-rect.size.width-margin rect.origin.y = margin case .leftBottom: rect.origin.x = dismissVideo ? -rect.size.width : margin rect.origin.y = size.height-rect.size.height-margin case .rightBottom: rect.origin.x = dismissVideo ? size.width : size.width-rect.size.width-margin rect.origin.y = size.height-rect.size.height-margin } to?.view.alpha = 0.0 UIView.animate(withDuration: self.config.duration, animations: { if dismissVideo { self.config.playLayer?.playView?.alpha = 0.0 } self.config.playLayer?.playView?.frame = rect }) { [unowned self] (_) in if dismissVideo { (self.config as? MMPlayerPassViewPresentConfig)?._dismissGesture = true self.config.playLayer?.setCoverView(enable: true) self.to?.dismiss(animated: true, completion: nil) } } }willSet + newValueを使う
@IBOutlet weak var okButton: UIButton! { willSet { // OKボタンの修飾 newValue.layer.borderColor = UIColor.white.cgColor newValue.layer.borderWidth = 1.0 newValue.layer.cornerRadius = 5.0 } }
- 投稿日:2019-10-24T15:04:55+09:00
[Swift 5] スクロールに応じてページング対応APIから1ページずつコンテンツを取得する方法
Requestパラメータでpageを指定すれば指定したページの情報を取得できるようなAPIがあるとして、
そのAPIから取得したコンテンツをアプリでページングして表示する実装の備忘録です。読み込み状態を定義
/// APIの読み込み状況 enum APILoadingStatus { case initial case fetching case loadMore case full case error }Viewでの実装
final class ViewController: UIViewController { /// APIの最終取得Page private var pageOfLastFetched = 1 /// APIの読み込み状況 private var loadStatus: APILoadingStatus = .initial }インクリメントして使うPage数と、読み込み状態を保持するプロパティを宣言します。
// APIを使用してコンテンツを取得する func fetchContents() { // 読み込み中またはもう次に記事がない場合にはapiを叩かない if loadStatus == .fetching || loadStatus == .full { return } // loadStatusを読み込み中に変更 loadStatus = .fetching // 記事一覧を取得するAPIを叩く repository.getContents(page: pageOfLastFetched) { [weak self] result in guard let self = self else { return } switch result { case .success(let contents): if contents.isEmpty { // 結果が空だった=もう続きの記事がないのでloadStatusをfullにする self.loadStatus = .full return } self.pageOfLastFetched.increment() // +1 // 記事取得が終わった段階でloadStatusをloadMoreにする self.loadStatus = .loadMore case .failure(let error): // loadStatusをエラーにする self.loadStatus = .error // TODO: Error handling. print(error) } } }APIをコールするメソッドを定義します。
成功したらPage数をインクリメントして、最後までFetchしたら読み込み状態を.full
にして以後APIをコールしないようにします。// MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { // 一定割合スクロールすると次のPageを読み込む let currentOffsetY = scrollView.contentOffset.y let maximumOffset = scrollView.contentSize.height - scrollView.frame.height let distanceToBottom = maximumOffset - currentOffsetY // 最下部まで500pxを切ったら次のページを取得する if distanceToBottom < 500 { fetchContents() } }ページングするタイミングですが、今回は画面最下部500pxを切るところまでスクロールされたらAPIでFecthするようにしました。
- 投稿日:2019-10-24T15:02:05+09:00
【Swift】Combineで一つのPublisherの出力結果を共有するメソッドやクラス�の違い(share, multicast, Future)
Combine.frameworkを使用していると
複数のSubscriber
で同じ処理結果を共有したい場合があります。例えば
- ネットワーク通信を介したデータの取得
- 画像のダウンロード
といった重い処理を繰り返し実行すると
メモリをたくさん使用したり
時間がかかるためユーザ体験を損なってしまう可能性もあります。Combine.frameworkでは
そのような時に利用できるメソッドやクラスが用意されています。今回はそういったメソッドやクラスの違いについて
見てみたいと思います。下記のサンプルコードは全てPlayground上で実行しています。
share
share
メソッドでは
subscribeする度に新しいPublisher
を生成するのではなく
Publisher
の参照を共有するようにします。
Publisher
はだいたいがstruct
であるため
関数の引数で使用される場合やプロパティに代入する際は
コピーを生成します。shareメソッドを呼ぶと戻り値には
Publishers.Share
という
クラスのインスタンスが返ってきます。
Publishers.Share
https://developer.apple.com/documentation/combine/publisher/3204754-shareこのクラスはクラスが生成されるよりも
前(上流)のPublisher
を保持することで
既存のPublisher
を使用することができます。最初のsubscribeで
Publisher
は処理を開始し値を流し始めます。そしてそれ以降のsubscribeからは
同じPublisher
からの値の出力を受け取ることができます。それでは具体的な例として
ネットワークリクエストの結果を
複数のSubscriber
で共有する例を見ていきたいと思います。var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .share() // **ここでshareを呼んでいる** print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 2回目 shared: receive value: (13761 bytes) subscription1 receiveValue: '13761 bytes' subscription2 receiveValue: '13761 bytes' shared: receive finished出力結果を見てみると
1回目のsubscribeではsubscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimitedと(上流の)
Publisher
へsubscribeしていますが2回目の場合
subscribe 2回目 shared: receive value: (13761 bytes) subscription1 receiveValue: '13761 bytes' subscription2 receiveValue: '13761 bytes' shared: receive finishedと
1回目のsubscribeで出力されていた
shared: receive subscription: (DataTaskPublisher)
shared: request unlimited
がなく
subscribeされていない
ことがわかりました。それでも2回目のsubscribeも値を受け取っています。
このような結果から
Publisher
は一つしか存在していないことがわかります。では
share
がなかった場合を見てみます。var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") //.share() // **コメントアウト** print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13761 bytes) subscription2 receiveValue: '13761 bytes' shared: receive finished shared: receive value: (13763 bytes) subscription1 receiveValue: '13763 bytes' shared: receive finishedとなり
2回目のsubscribe時でもsubscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimitedと出力されています。
つまり
Publisher
のコピーが生成され
Publisher
が2つ存在していることがわかります。このように
share
を使うことで
不要なPublisher
インスタンスを生成しなくなりました。ただし
share
には注意点もあります。
share
では
subscribeする前に出力された値を再度出力しません。つまり
参照を保持するタイミングによっては
期待したい値が得られないかもしれません。※
参照を保持する時点で
Publisher
がcompletionしていた場合は
finished(completionイベント)のみが返ってきます。
※ sink(receiveCompletion:)の方に値が流れてきます。下記の例を見てみます。
var cancellables: Set<AnyCancellable> = [] let shared = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .share() print("subscribe 1回目") shared .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { print("subscribe 2回目") shared .sink(receiveCompletion: { print("subscription2 receiveCompletion \($0)")}, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } // 出力結果 subscribe 1回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13750 bytes) subscription1 receiveValue: '13750 bytes' shared: receive finished subscribe 2回目 subscription2 receiveCompletion finished出力結果からもわかるように
2回目のsubscribe
のタイミングを遅らせると
Publisher
はすでにcompletionしているため
finished(completionイベント)のみを受け取っていることがわかります。multicast
上記の
share
ではタイミングによっては
値を受け取れない可能性がありました。そこで
Combineでは値の出力を能動的にコントロールできる
multicast
というメソッド(戻り値はPublishers.Multicast
クラス)があります。https://developer.apple.com/documentation/combine/publisher/3204734-multicast
https://developer.apple.com/documentation/combine/publisher/3204733-multicast
https://developer.apple.com/documentation/combine/publishers/multicastこれは
ConnectablePublisher
プロトコルに適合しています。/// A publisher that uses a subject to deliver elements to multiple subscribers. final public class Multicast<Upstream, SubjectType> : ConnectablePublisher where Upstream : Publisher, SubjectType : Subject, Upstream.Failure == SubjectType.Failure, Upstream.Output == SubjectType.Output {https://developer.apple.com/documentation/combine/connectablepublisher
このプロトコルは
connect
というメソッドを有しており
connect
を呼び出して初めてPublisher
はSubscriber
を受け取りを処理を開始します。
https://developer.apple.com/documentation/combine/connectablepublisher/3204394-connect
share
の例を
multicast
に変えて違いを見てみます。var cancellables: Set<AnyCancellable> = [] let multicasted = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .multicast { PassthroughSubject<Data, URLError>() } print("subscribe 1回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) multicasted .connect() .store(in: &cancellables) // 出力結果 subscribe 1回目 subscribe 2回目 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13741 bytes) subscription1 receiveValue: '13741 bytes' subscription2 receiveValue: '13741 bytes' shared: receive finished上記の結果では
share
と同じ結果を取得できました。では
ここでconnect
をコメントアウトしてみると... //multicasted // .connect() // .store(in: &cancellables) // 出力結果 subscribe 1回目 subscribe 2回目となり
subscribe
してはいるものの
Publisher
はまだ処理を実行していません。このことから
multicast
では
connect
を呼ばないと
処理を実行しないことがわかります。では
2回目のsubscribeを少し遅らせ
connect
を呼び出してみた場合を今度は見てみます。var cancellables: Set<AnyCancellable> = [] let multicasted = URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .multicast { PassthroughSubject<Data, URLError>() } print("subscribe 1回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { print("subscribe 2回目") multicasted .sink( receiveCompletion: { _ in }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("connect") multicasted .connect() .store(in: &cancellables) } // 出力結果 subscribe 1回目 subscribe 2回目 connect shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (13745 bytes) subscription1 receiveValue: '13745 bytes' subscription2 receiveValue: '13745 bytes' shared: receive finished
connect
が呼ばれる前にsubscribeしているため
2回目のsubscribeでもデータがきちんと取得できています。これで
share
で起きていた問題は緩和できそうですが
ちょっと書き方が複雑であったり
connect
を呼ぶタイミングによっては
subscribeし損ねてしまう可能性もまだ残っています。他のライブラリでは出力された値を再度流してくれるメソッドがあります。
例えば
RxSwiftではshareReplay
というメソッドと利用することで
指定したサイズ分の流れてきたデータをキャッシュすることができ
subscribe時にその値が流れてくるようにできますが
Combineでは現状存在しません。もし必要な場合は↓のように独自の実装が必要になります。
https://github.com/tcldr/Entwine/blob/master/Sources/Entwine/Operators/ShareReplay.swift※
補足になりますが
ConnectablePublisher
プロトコルには
autoconnect
というメソッドもありこれを呼び出した場合は
subscribeすると自動で処理を開始して値の出力を始めます。https://developer.apple.com/documentation/combine/connectablepublisher/3235788-autoconnect
Future
少し形が違いますが
share
やmulticast
と同様に
処理の結果を複数のSubscriber
で共有できます。https://developer.apple.com/documentation/combine/future
他と違う特徴としては
処理が実行されるタイミングが初めてSubscribeされたタイミング
ではなく
Future
インスタンスが生成されたタイミングになります。
下記の例を見ていきます。
var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { promise in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { promise(.failure(error)) } }, receiveValue: { promise(.success($0)) }).store(in: &cancellables) } // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited shared: receive value: (14676 bytes) shared: receive finished上記のようにsubscribeしていない状態でも
クロージャ内の処理が実行されていることがわかります。ではsubscribeしてみます。
var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { promise in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { promise(.failure(error)) } }, receiveValue: { promise(.success($0)) }).store(in: &cancellables) } print("subscribe 1回目") future .sink(receiveCompletion: { print("subscription1 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) print("subscribe 2回目") future .sink(receiveCompletion: { print("subscription2 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 1回目 subscribe 2回目 shared: receive value: (14676 bytes) subscription1 receiveValue: '14676 bytes' subscription1 receiveCompletion: 'finished' subscription2 receiveValue: '14676 bytes' subscription2 receiveCompletion: 'finished' shared: receive finishedこのように
Future
内の処理は1回しか行われていませんが
2つのSubscriber
はどちらも値を受け取ることができています。では処理が完了した後に
subscribeした場合はどうでしょうか?var cancellables: Set<AnyCancellable> = [] let future = Future<Data, URLError> { fulfill in URLSession.shared .dataTaskPublisher(for: URL(string: "https://www.google.com")!) .map(\.data) .print("shared") .sink(receiveCompletion: { if case .failure(let error) = $0 { fulfill(.failure(error)) } }, receiveValue: { fulfill(.success($0)) }).store(in: &cancellables) } print("subscribe 1回目") future .sink(receiveCompletion: { print("subscription1 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription1 receiveValue: '\($0)'") }) .store(in: &cancellables) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("subscribe 2回目") future .sink(receiveCompletion: { print("subscription2 receiveCompletion: '\($0)'") }, receiveValue: { print("subscription2 receiveValue: '\($0)'") }) .store(in: &cancellables) } // 出力結果 shared: receive subscription: (DataTaskPublisher) shared: request unlimited subscribe 1回目 shared: receive value: (14676 bytes) subscription1 receiveValue: '14676 bytes' subscription1 receiveCompletion: 'finished' shared: receive finished subscribe 2回目 subscription2 receiveValue: '14676 bytes' subscription2 receiveCompletion: 'finished'上記のように2回目のsubscribeでも
値を受け取れていることがわかりました。
Future
の注意点としては
処理が実行されるタイミングが
Future
インタンスが生成された時点になりますので
subscribeされた後に必要な処理などがある場合は
予期せぬ挙動に遭遇する可能性があります。まとめ
出力結果を共有できるメソッドやクラスについて見ていきました。
メモリの使用量や通信量を抑えることができるという点で
非常に有用なものですがいつPublisherは処理を開始して値を出力し始めるのか?
いつsubscribeすると値を受け取ることができるのか?といったことを知らないと
「何が起きているんだ。。。?」
と悩んでしまうような事象に出くわすかもしれません。そのような自体にならないためにも
違いを比較して見ていくことは大切だなと感じています。Combine.frameworkはメソッドやクラスがたくさんあり
全てをは把握することは大変ですが
ある分類に分けて少しずつみていくと
効率的に把握できるのかなと思います。?もし間違いなどございましたらご指摘いただけますと助かります??♂️
- 投稿日:2019-10-24T14:20:20+09:00
swift入門の為のメモ
実際には運用するアプリの制作入門としてのメモを随時記載
1、まずはメニュー有りき
// SubViewController へ遷移するために Segue を呼び出す performSegueWithIdentifier("showDetailView",sender: nil) // Segueで遷移時の処理 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) { if (segue.identifier == "showDetailView") { let secondVC: ViewController4 = (segue.destinationViewController as? ViewController4)! // showDetailViewのtextに選択した文字列を設定する print("segue") print(view.frame.height) //secondVC.itemid = selectedText } }2、アプリ内記憶処理
3、sqlite3の利用
4、サーバーサイドのシステムとの連動
5、サーバーサイドデーター利用
- 投稿日:2019-10-24T12:35:15+09:00
iOS 13以上でサイレントプッシュのdidReceiveRemoteNotificationが呼ばれない問題
最近、個人的な都合で英語生活してるので、本記事も両言語で書いてみます。と言っても大した量じゃありませんが。
I'd like to write this article in both language Japanese and English due to personal reasons these days.
日本語版
didReceiveRemoteNotificationメソッドがiOS 13以上で呼ばれない理由
特定の手続きをしないと、iOS 13以上でサイレントプッシュを使っている時にAppDelegateのdidReceiveRemoteNotificationが呼ばれないことがわかりました。
AppDelegate.swiftfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("PUSH arrived.") completionHandler(.noData) }サーバー側でヘッダーに
apns-push-type: background
を付けるか、もしくは以下のようにペイロードJSONのaps
部分に空のalert
を付けると、無事呼ばれるようになりました。body.json{ "aps": { "alert": {}, "content-available": 1 } }WWDC2019で発表された正式な方法らしい前者が理想ですが、もしサーバーがカスタムヘッダーをサポートしていない場合は後者のほうが簡単です。
参考文献
- iOS 13 beta 7 Silent Push Bug |Apple Developer Forums
- iOS 13 and Xcode 11 Changes That Affect Push Notifications
- APNS Priority must be set to 5 for background notifications
English ver.
The reason your didReceiveRemoteNotification method won't work on iOS 13 or above
I found that didReceiveRemoteNotification method on AppDelegate without a specific procedure won't be called if you use a background push on iOS 13 or above.
AppDelegate.swiftfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { print("PUSH arrived.") completionHandler(.noData) }The solution I finally found is adding
apns-push-type: background
to your header field, or adding an emptyalert
field in theaps
section in your custom payload as follows.body.json{ "aps": { "alert": {}, "content-available": 1 } }The former is ideal because it's an official way that was revealed in WWDC2019, but the latter would be easier if your server doesn't support any customizations of push headers.
References
- iOS 13 beta 7 Silent Push Bug |Apple Developer Forums
- iOS 13 and Xcode 11 Changes That Affect Push Notifications
- APNS Priority must be set to 5 for background notifications
- 投稿日:2019-10-24T11:39:50+09:00
ReactiveX入門
はじめに
- ReactiveX の概念を理解するためにまとめた資料です
- 社内クローズドな勉強会で話した内容を Qiita 向けに修正しています
- 前提としてデザインパターンの知識がちょっと必要です
- サンプルコードはほぼ Swift + RxSwift です
ReactiveXとは
ReactiveXとは
ReactiveX (以降 Rx) は Observable シーケンスを使った非同期でイベントベースのプログラムを構成するためのライブラリ。
デザインパターンの Observer パターン を下記の2点で拡張したもの。
- データやイベントのシーケンス をサポートする
- 宣言的にシーケンスを構成できるようにするための オペレータ を追加する
また、低レベルなスレッディング、同期、スレッドセーフ、並行データ構造、ノンブロッキングI/Oのような懸念事項を抽象化してくれる。
(引用元: http://reactivex.io/intro.html)
Rx は Observer パターンの拡張
- データやイベントのシーケンスをサポートする
- 宣言的にシーケンスを構成できるようにするためのオペレータを追加する
【復習】Observerパターン
Observerパターン
オブジェクトのイベントを他のオブジェクトへ通知するためのパターン。GoF による デザインパターン の一つ。
- Observerable通知する側のオブジェクト=監視可能なオブジェクト。Observer に監視される。
- Observer通知される側のオブジェクト=監視するオブジェクト。Observerable を 監視する。
Observerパターンの例 (Swift)
observer-patternimport Foundation // MARK: - Interfaces protocol IObserver: class { associatedtype Value func notify(_ value: Value) } protocol IObservable: class { associatedtype Value associatedtype Observer: IObserver where Observer.Value == Value func registerObserver(_ observer: Observer) func unregisterObserver(_ observer: Observer) func notifyObservers(_ value: Value) } // MARK: - Implementations class IntObserver: IObserver { typealias Value = Int let name: String init(name: String) { self.name = name } func notify(_ value: Value) { print("\(name) -> \(value)") } } class IntObservable: IObservable { typealias Value = Int typealias Observer = IntObserver func registerObserver(_ observer: Observer) { observers.append(observer) } func unregisterObserver(_ observer: Observer) { observers.removeAll(where: { $0 === observer }) } func notifyObservers(_ value: Observer.Value) { observers.forEach { $0.notify(value) } } private var observers: [Observer] = [] } // MARK: - Examples let observable = IntObservable() // register observers observable.registerObserver(IntObserver(name: "A")) observable.registerObserver(IntObserver(name: "B")) //notify observable.notifyObservers(10) observable.notifyObservers(20)resultA -> 10 B -> 10 A -> 20 B -> 20Observable ≒ データやイベントのシーケンス
observable.notifyObservers(10) observable.notifyObservers(20)
- Observer には Observable から 10, 20 というデータが順番に非同期に通知される
- また、ここで通知されるものはデータに限らず、例えばボタンクリックのようなイベントかもしれない
- つまり、Observable は データやイベントのシーケンス とみなせる
データやイベントの非同期なシーケンスに対して順番に反応する Reactive Programing
【復習】Iterator パターン
Iterator パターン
- 集約したオブジェクトを列挙する手段を提供するデザインパターン。これも GoF の一つ。
- モダンなプログラミング言語のコレクションコンテナでは大抵実装されている。
Iterator パターンの例 (Swiftのコレクション)
Swift の配列などのコレクションは Iterator パターンを定義した
Sequence
というプロトコル(≒ Interface)に準拠している。swift-sequenceprotocol IteratorProtocol { associatedtype Element mutating func next() -> Element? } protocol Sequence { associatedtype Element associatedtype Iterator: IteratorProtocol func makeIterator() -> Iterator }Iterator パターンの例 (Swiftのコレクション)
Sequence
プロトコルに準拠していると Iterator によりコレクションの要素を次のように列挙することができる。next()
がnil
(null
) を返すと列挙は完了する。array-iteratorlet array = [1, 2, 3, 4, 5] var iterator = array.makeIterator() while let next = iterator.next() { print(next) }
- (おまけ)上記のコードは Swift の
for in
構文でシンプルに記述することができる。for-inlet array = [1, 2, 3, 4, 5] for element in array { print(element) }Observer/Iterator パターンのまとめ
Observer/Iterator パターンのまとめ
- Observer パターンの Observable はデータやイベントの シーケンス とみなせる
- Iterator パターンはシーケンスの要素を 次へ (Next) により順番に得ることができる
- また、次の要素がない場合はシーケンスの 完了 (Complete) となる
コレクション と Observable (Rx)
コレクション と Observable (Rx) ? どちらもシーケンス
コレクションは Iterator パターンにより要素を順番に処理できる。
let array = [1, 2, 3, 4, 5] var iterator = array.makeIterator() while let next = iterator.next() { print(next) }これを Rx の Observable の世界にするとこうなる。
let observable = Observable.of(1, 2, 3, 4, 5) observable.subscribe(onNext: { next in print(next) }? 似てるよね!
コレクション と Observable (Rx) の違い
(引用元: https://www.atmarkit.co.jp/fdotnet/introrx/introrx_01/introrx_01_01.html)
? Rx の Observable は時間軸に乗る非同期なストリーム(小川)
コレクション と Observable (Rx) の違い (ソースコードで比べる)
コレクションでは
next()
で能動的に次の要素を取りに行く。let array = [1, 2, 3, 4, 5] var iterator = array.makeIterator() while let next = iterator.next() { print(next) }Rx の Observable では、それを 購読 (Subscribe) し、次の要素が放出されるのを
onNext
で待つ。let observable = Observable.of(1, 2, 3, 4, 5) observable.subscribe(onNext: { next in print(next) }Rx のマーブルダイアグラム
Rx の Observable はマーブルダイアグラムにより図示される。
Rx とは Observer パターンの拡張
- データやイベントのシーケンスをサポートする
- 宣言的にシーケンスを構成できるようにするためのオペレータを追加する
オペレータ
コレクションのオペレータ
モダンなプログラミング言語ではコレクションに対する様々なオペレータとサポートする。
- 射影 :
map
,flatMap
, ...- 抽出 :
filter
,take
, ...- 合成 :
concat
,zip
, ...- 集計 :
reduce
,distinct
, ...- 並び替え :
sort
,reverse
, ...(参考: https://qiita.com/amay077/items/9d2941283c4a5f61f302)
コレクションのオペレータの例 (Swift)
整数のコレクションに次のオペレーション行う。
- 全ての要素に3を足す
- そこから偶数のみを抽出する
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] array.map { $0 + 3 } // [4, 5, 6, 7, 8, 9, 10, 11, 12, 13] .filter { $0.isMultiple(of: 2) } // [4, 6, 8, 10, 12] .subscribe(onNext: { print($0) }Observable のオペレータ
ほぼ同じ!Observable もシーケンス。Rx からは様々なオペレータが提供されている。
let observable = Observable.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) observable.map { $0 + 3 } // [4, 5, 6, 7, 8, 9, 10, 11, 12, 13] .filter { $0.isMultiple(of: 2) } // [4, 6, 8, 10, 12] .subscribe(onNext: { print($0) }Observable の具体的な例 (Spread Sheet)
みんな大好きスプレッドシート。
let a1 = Observable.of(1) let b1 = Observable.of(2) let c1 = Observable.combineLatest(a1, b1).map { $0 + $1 }Observable の具体的な例 (UI)
検索バーにキーワードを入力するたびに検索結果が表示される(インクリメンタルサーチ)
// テキストフィールドからの入力が変化する度に、その入力内容が放出される textField.rx.text.asObservable() // 連続入力でAPIを叩きすぎないようにするために200msec以内の連続入力は無視する .debounce(.milliseconds(200), scheduler: MainScheduler.instance) // 入力内容で検索APIを実行 .flatMap { keyword in API.search(by: keyword).asObservable() } // その結果を購読し、処理する(結果一覧を表示する、など) .subscribe(onNext: { result in print(result) })Rxの起源
- Microsoft Research の実験的なプロジェクトとして始まる
- 2009年に .NET Framework 向けの Rx.NET を提供開始
- 2011年に正式な製品に昇格し、以降各プラットフォームに移植される
- Rx 全体の開発を現在リードしているのは Netflix (MSでRxやってた人が転職した先)
- Rx のオペレータは 2007年にリリースされた C# 3.0 の新機能 LINQ の非同期/イベント版といえる
(参考: https://www.atmarkit.co.jp/fdotnet/introrx/introrx_01/introrx_01_01.html)
(参考: https://www.buildinsider.net/column/kawai-yoshifumi/004)Rxの対応プラットフォーム
触れないプラットフォームなど、(ほぼ)ない! Let's play with Rx!
言語/PF Rx Java RxJava Java (Android) RxAndroid JavaScript RxJS C# Rx.NET C#(Unity) UniRx Scala RxScala Clojure RxClojure C++ RxCpp Lua RxLua Ruby Rx.rb Python RxPY Go RxGo Groovy RxGroovy JRuby RxJRuby Kotlin RxKotlin Swift RxSwift Swift (iOS/macOS) RxCocoa PHP RxPHP Elixir reaxive Dart RxDart おまけ
脱Rxの兆し
iOS/Android 界隈では脱 Rx の兆しが...
- iOS : Apple が Swift 用 RP フレームワーク Combine を提供開始
- iOS13以降で利用可能。Swift UI のためのフレームワークでもある。
- Android : Kotlin の Coroutine 機能に RP 的機能が増えてきている
- Channel, Flow, ...
Rx is not FRP
ReactiveX はたまに Functional Reactive Programming (FRP) と称されるが、それは 誤り 。ReactiveX は Functional で Reactive かもしれないが、FRP とは別のもの。
Difference FRP operates on values that change continuously over time
時間とともに継続的に変化する値を操作するReactiveX operates on discrete values that are emitted over time
時間の経過とともに放出される離散的な値を操作するなるほど、わからん\(^o^)/
(参考: https://twitter.com/ReactiveX/status/483625917491970048)