Swift で Audio Queue Services を使って再生する
Audio Queue Services Programming Guide - Playing Audio を参考にSwiftでAudio Queue Serviceを使用する。
環境
準備
AudioToolbox をインポートする。
import AudioToolbox
Audio Queue Service を使用する手順
- 状態・フォーマット・パス情報を管理する構造体を定義する
- 実際の再生を行うオーディオキューコールバックを作成する
- オーディオキューバッファのサイズを決定するコードを書く
- プレイバックするオーディオファイルを開き、オーディオデータのフォーマットを取得する
- プレイバックオーディオキューを作成し、プレイバックに設定する
- オーディオキューバッファを生成し、エンキューする
- リソースの解放
状態・フォーマット・パス情報を管理するクラス(構造体)を定義
オーディオフォーマットとオーディオキューの情報を管理するクラスを定義する。 状態の更新を行うのでクラス(参照型)で定義する。
let kNumberBuffers = 3 class AQPlayerState { var mDataFormat: AudioStreamBasicDescription var mQueue: AudioQueueRef var mBuffers: [AudioQueueBufferRef] var mAudioFile: AudioFileID var bufferByteSize: UInt32 var mCurrentPacket: Int64 var mNumPacketsToRead: UInt32 var mPacketDescs: UnsafeMutablePointer<AudioStreamPacketDescription> var mIsRunning: Bool // 見やすさのためinit内で初期化する init() { mDataFormat = AudioStreamBasicDescription() mQueue = nil mBuffers = [AudioQueueBufferRef](count: kNumberBuffers, repeatedValue: nil) mAudioFile = nil bufferByteSize = 0 mCurrentPacket = 0 mNumPacketsToRead = 0 mPacketDescs = nil mIsRunning = false } }
オーディオキューコールバックの作成
このコールバック関数は主に以下の3つのことをする
- 指定された分のデータをオーディオファイルから読み込み、それをオーディオキューバッファに入れる
- バッファキューにオーディオバッファキューをエンキューする
- 読み込むデータがない場合にオーディオキューに停止するように伝える
以下がサンプルに示されている基本的なコールバック関数になる。
func HandleOutputBuffer(aqData: UnsafeMutablePointer<Void>, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) { // AQPlayerStateにキャスト let aqdata = UnsafeMutablePointer<AQPlayerState>(aqData).memory // 再生中でない場合は何もしない guard aqdata.mIsRunning else { return } // AudioFileReadPackets deprecated の変更により // numBytesReadFromFile を設定する var numBytesReadFromFile = aqdata.bufferByteSize var numPackets = aqdata.mNumPacketsToRead // オーディオファイルからデータを読み込む AudioFileReadPacketData(aqdata.mAudioFile, false, &numBytesReadFromFile, aqdata.mPacketDescs, aqdata.mCurrentPacket, &numPackets, inBuffer.memory.mAudioData) if (numPackets > 0) { inBuffer.memory.mAudioDataByteSize = numBytesReadFromFile // オーディオキューバッファをエンキューする AudioQueueEnqueueBuffer(aqdata.mQueue, inBuffer, (aqdata.mPacketDescs != nil ? numPackets : 0), aqdata.mPacketDescs) aqdata.mCurrentPacket += Int64(numPackets) } else { // オーディオキューを停止する AudioQueueStop(aqdata.mQueue, false) aqdata.mIsRunning = false } }
再生するオーディオキューバッファのサイズを決定する
func DeriveBufferSize(ASBDesc: AudioStreamBasicDescription, maxPacketSize: UInt32, seconds: Float64, outBufferSize: UnsafeMutablePointer<UInt32>, outNumPacketsToRead: UnsafeMutablePointer<UInt32>) { let maxBufferSize: UInt32 = 0x50000 // 320 KB let minBufferSize: UInt32 = 0x4000 // 16 KB if ASBDesc.mFramesPerPacket != 0 { let numPacketsForTime = ASBDesc.mSampleRate / Float64(ASBDesc.mFramesPerPacket) * seconds outBufferSize.memory = UInt32(numPacketsForTime) * maxPacketSize } else { outBufferSize.memory = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize } if outBufferSize.memory > maxBufferSize && outBufferSize.memory > maxPacketSize { outBufferSize.memory = maxBufferSize } else { if outBufferSize.memory < minBufferSize { outBufferSize.memory = minBufferSize } } outNumPacketsToRead.memory = outBufferSize.memory / maxPacketSize }
再生するオーディオファイルを開く
let filePath = "music file path" let url: CFURL = NSURL(fileURLWithPath: filePath) let result = AudioFileOpenURL(url, .ReadPermission, 0, &aqData.mAudioFile) assert(result == noErr, "AudioFileOpenURL Error: \(result)")
オーディオデータのフォーマットを取得
let result = AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyDataFormat, &dataFormatSize, &aqData.mDataFormat) assert(result == noErr, "AudioFileGetProperty Error: \(result)")
プレイバックオーディオキューの生成
let result = AudioQueueNewOutput(&aqData.mDataFormat, HandleOutputBuffer, &aqData, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, // アップルに予約されている。0でなければならない &aqData.mQueue) assert(result == noErr, "AudioQueueNewOutput Error: \(result)")
プレイバックオーディオキューのサイズを設定
var maxPacketSize: UInt32 = 0 var propertySize = UInt32(sizeof(UInt32.self)) let result = AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize) DeriveBufferSize(aqData.mDataFormat, maxPacketSize: maxPacketSize, seconds: 0.5, outBufferSize: &aqData.bufferByteSize, outNumPacketsToRead: &aqData.mNumPacketsToRead)
Packet Description配列のメモリを割りあてる
// Allocating Memory for a Packet Descriptions Array let isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 || aqData.mDataFormat.mFramesPerPacket == 0 if isFormatVBR { let size = Int(aqData.mNumPacketsToRead * UInt32(sizeof(AudioStreamPacketDescription.self))) aqData.mPacketDescs = UnsafeMutablePointer<AudioStreamPacketDescription>.alloc(size) } else { aqData.mPacketDescs = nil }
プレイバックオーディオキューのMagic Cookieを設定
var cookieSize = UInt32(sizeof(UInt32.self)) let result = AudioFileGetPropertyInfo(aqData.mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, nil) if result != noErr { let magicCookie = UnsafeMutablePointer<CChar>.alloc(Int(cookieSize)) AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie) AudioQueueSetProperty(aqData.mQueue, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize) free(magicCookie) }
最初に再生するオーディオキューバッファを用意する
aqData.mIsRunning = true aqData.mCurrentPacket = 0 for i in (0..<kNumberBuffers) { AudioQueueAllocateBuffer(aqData.mQueue, aqData.bufferByteSize, &aqData.mBuffers[i]) HandleOutputBuffer(&aqData, inAQ: aqData.mQueue, inBuffer: aqData.mBuffers[i]) }
オーディオキューを開始する
AudioQueueStart(aqData.mQueue, nil)
Command Lineで実行する場合は以下も必要。iOSの場合は不要。
repeat { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, false) } while aqData.mIsRunning CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, false)
再生後の処理
AudioQueueDispose(aqData.mQueue, true) // Dispose audio queue AudioFileClose(aqData.mAudioFile) // Close audio file free(aqData.mPacketDescs) // Release the packet description
参考URL
デリゲートメソッドをRxSwift対応させる方法
この例ではUIWebViewDelegateメソッドをRxSwift対応させる。
環境
準備
RxCocoa、RxSwiftをインポート。
import RxCocoa import RxSwift
今のところ以下のキャスト時に使用する関数はパブリックになっていないので、RxCocoaからコピーしてくる。
@noreturn func rxFatalError(lastMessage: String) { fatalError(lastMessage) } func castOptionalOrFatalError<T>(value: AnyObject?) -> T? { if value == nil { return nil } let v: T = castOrFatalError(value) return v } func castOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T { guard let returnValue = object as? T else { throw RxCocoaError.CastingError(object: object, targetType: resultType) } return returnValue } func castOptionalOrThrow<T>(resultType: T.Type, _ object: AnyObject) throws -> T? { if NSNull().isEqual(object) { return nil } guard let returnValue = object as? T else { throw RxCocoaError.CastingError(object: object, targetType: resultType) } return returnValue } func castOrFatalError<T>(value: AnyObject!, message: String) -> T { let maybeResult: T? = value as? T guard let result = maybeResult else { rxFatalError(message) } return result } func castOrFatalError<T>(value: Any!) -> T { let maybeResult: T? = value as? T guard let result = maybeResult else { rxFatalError("Failure converting from \(value) to \(T.self)") } return result }
DelegateProxyクラスを作成
対応させるデリゲート(UIWebViewDelegate)のプロキシークラスを作成する。
class RxUIWebViewDelegateProxy: DelegateProxy, UIWebViewDelegate, DelegateProxyType { class func currentDelegateFor(object: AnyObject) -> AnyObject? { let webView: UIWebView = castOrFatalError(object) return webView.delegate } class func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) { let webView: UIWebView = castOrFatalError(object) webView.delegate = castOptionalOrFatalError(delegate) } }
Reactive ラッパー
UIWebViewのエクステンションとして、Reactiveラッパーを用意する。今回は以下のUIWebViewDelegateメソッドをラップする。
- webViewDidStartLoad(webView: UIWebView)
- webViewDidFinishLoad(webView: UIWebView)
- webView(webView: UIWebView, didFailLoadWithError error: NSError?)
extension UIWebView { // Reactive wrapper for `delegate`. public var rx_delegate: DelegateProxy { return proxyForObject(RxUIWebViewDelegateProxy.self, self) } // Reactive wrapper for webViewDidStartLoad(_:) public var rx_webViewDidStartLoad: Observable<UIWebView> { let sel = #selector(UIWebViewDelegate.webViewDidStartLoad(_:)) let o = rx_delegate .observe(sel) .map { a in return try castOrThrow(UIWebView.self, a[0]) } return o } // Reactive wrapper for webViewDidFinishLoad(_:) public var rx_webViewDidFinishLoad: Observable<UIWebView> { let sel = #selector(UIWebViewDelegate.webViewDidFinishLoad(_:)) let o = rx_delegate .observe(sel) .map { a in return try castOrThrow(UIWebView.self, a[0]) } return o } // Reactive wrapper for webView(_:didFailLoadWithError:) public var rx_webViewDidFailLoadError: Observable<(UIWebView, NSError?)> { let sel = #selector(UIWebViewDelegate.webView(_:didFailLoadWithError:)) let o = rx_delegate .observe(sel) .map { a -> (UIWebView, NSError?) in let webview = try castOrThrow(UIWebView.self, a[0]) let error = try castOptionalOrThrow(NSError.self, a[1]) return (webview, error) } return o } }
戻り値のあるデリゲートメソッドの場合
DelegateProxy.setForwardToDelegate(_:retainDelegate:) を使用して通常と同じように扱う。
使用例
class ExampleViewController: UIViewController, UIWebViewDelegate { @IBOutlet weak var webView: UIWebView! let disposeBag = DisposeBag() override func viewDidLoad() { webView .rx_webViewDidStartLoad .subscribeNext { webview in print("webViewDidStartLoad", webview) }.addDisposableTo(disposeBag) webView .rx_webViewDidFinishLoad .subscribeNext { webview in print("webViewDidFinishLoad", webview) }.addDisposableTo(disposeBag) webView .rx_webViewDidFailLoadError .subscribeNext { (webview, error) in print("webViewDidFailLoadError", webview, error) }.addDisposableTo(disposeBag) webView.rx_delegate.setForwardToDelegate(self, retainDelegate: false) let url = NSURL(string: "https://xxx")! let request = NSURLRequest(URL:url) webView.loadRequest(request) } func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { print(request.URL?.absoluteString,) return true } }
参考URL
Swift でヒマワリを描く
に触発されて、試しにSwiftで書いてみた。
準備
16進数カラーコードを UIColor に変換する必要があるので、 SwiftでHexColor(#34495eみたいなやつ) - Qiita を参考に、UIColorに簡易イニシャライザを追加する。
extension UIColor { convenience init(hexString: String, alpha: CGFloat) { let hex = hexString.stringByReplacingOccurrencesOfString("#", withString: "") let scanner = NSScanner(string: hex) var rgbValue: UInt32 = 0 if scanner.scanHexInt(&rgbValue) { self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgbValue & 0xFF00) >> 8) / 255.0, blue: CGFloat(rgbValue & 0xFF) / 255.0, alpha: alpha) } else { self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: alpha) } } convenience init(hexString: String) { self.init(hexString: hexString, alpha: 1.0) } }
mkCoords 関数
配置する円の座標を求める関数。
func mkCoords(n: Int) -> [CGPoint] { // Helper 関数 func coord(n: Int) -> CGPoint { return fromPolar(sqrt(CGFloat(n)), theta: 2.4 * CGFloat(n)) } func fromPolar(r: CGFloat, theta: CGFloat) -> CGPoint { return CGPointMake(r * cos(theta), r * sin(theta)) } return (1...n).map(coord) }
floret 関数
円を作成する関数。とりあえずCALayerにしておく。ここで使用しているカラーセットの定義はここからコピーしてきた。
func floret(r: CGFloat) -> CALayer { let layer = CAShapeLayer() let path = UIBezierPath(arcCenter: CGPointZero, radius: 0.6, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true).CGPath layer.path = path let n = floor(1.4 * sqrt(r)) % 9 let colors = ["#ffffe5","#fff7bc","#fee391","#fec44f", "#fe9929","#ec7014","#cc4c02","#993404", "#662506","#000000"].map(UIColor.init).reverse() as [UIColor] layer.fillColor = colors[Int(n)].CGColor return layer }
sunflower 関数
ひまわりのレイヤーを返す関数。
func sunflower(n: Int) -> [CALayer] { func florets(n: Int) -> [CALayer] { return (1...n).map { floret(sqrt(CGFloat($0))) } } return zip(mkCoords(n), florets(n)).map { (position, layer) in // 円の位置を変更 layer.position = CGPointMake(position.x + 100, position.y + 100) return layer } }
SunflowerView クラス
ひまわりを描画するView
class SunflowerView: UIView { override func drawRect(rect: CGRect) { let baseLayer = CALayer() baseLayer.frame = CGRectMake(0, 0, 200, 200) sunflower(2000).forEach { baseLayer.addSublayer($0) } // ちょっと拡大 baseLayer.transform = CATransform3DMakeScale(3.0, 3.0, 0.0) layer.addSublayer(baseLayer) } }
完成
SunflowerView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
をaddSubview等すれば完成。以下のようなひまわりが描画される。
注意
試す場合は、Playgroundでは時間がかかるので、Single View Application で行う方が良い(?)
参考URL
Swift 型消去 (Type Erasure)
try! Swiftで発表のあった型消去の話。
この例ではSwiftにおける型消去技法を使用して、結城さんの 増補改訂版Java言語で学ぶデザインパターン入門 Iteratorパターンを愚直にSwiftで実装する。
前提知識
- [増補改訂版Java言語で学ぶデザインパターン入門] Iteratorパターン
- Swift ジェネリクス・プロトコルの基礎
環境
Iteratorインターフェース
要素の数え上げをするインターフェース。
protocol Iterator { typealias Element func next() -> Element? }
Aggregateインターフェース
SwiftではJava版のように、以下のようにAggregateを定義することはできない。
protocol Aggregate { func iterator() -> Iterator }
この場合、下のようなエラーになる。
error: protocol 'Iterator' can only be used as a generic constraint because it has Self or associated type requirements
SwiftではAssociated Typeを使って定義されたプロトコル(抽象型)をそのまま使うことはできないので、AnyIteratorを返すようにする。
protocol Aggregate { typealias Element func iterator() -> AnyIterator<Element> }
AnyIteratorクラス(型消去法)
IteratorクラスのAssociated Typeを消去するためのラッパークラス。
Associated Typeからジェネリクス型に変換する。
class AnyIterator<Element>: Iterator { private let _next: () -> Element? init<Base: Iterator where Base.Element == Element>(_ base: Base) { _next = base.next } func next() -> Element? { return _next() } }
Bookクラス
本を表すクラス。
class Book { let name: String init(name: String) { self.name = name } func getName() -> String { return name } }
BookShelfクラス
本棚を表すクラス。
class BookShelf: Aggregate { var books: [Book] = [] func getBookAt(index: Int) -> Book? { return index < books.count ? books[index] : nil } func appendBook(book: Book) { books.append(book) } // Aggregateの実装 func iterator() -> AnyIterator<Book> { let bookShelfIterator = BookShelfIterator(bookShelf: self) return AnyIterator(bookShelfIterator) } }
BookShelfIterator
BookShelfをイテレートするクラス。
class BookShelfIterator: Iterator { let bookShelf: BookShelf var index: Int = 0 init(bookShelf: BookShelf) { self.bookShelf = bookShelf } // Iterator next func next() -> Book? { let book = bookShelf.getBookAt(index) index++ return book } }
実行例
let bookShelf = BookShelf() // 本棚(BookShelf)に本(Book)を入れる bookShelf.appendBook(Book(name: "Book A")) bookShelf.appendBook(Book(name: "Book B")) bookShelf.appendBook(Book(name: "Book C")) bookShelf.appendBook(Book(name: "Book D")) // bookをイテレートするAnyIterator<Book>を生成 let bookIterator = bookShelf.iterator() // イテレート while let book = bookIterator.next() { print(book.getName()) }
この出力は、
Book A Book B Book C Book D
参考URL
Note
RxMoya (RxSwift + Moya)
Swift Moyaでモヤっとしているネットワークレイヤーを解決 - タコさんブログ のRx編。
RxMoyaProviderでは、MoyaProviderにコールバッククロージャを与える代わりに、オブザーバブルなレスポンスを使用する。
今回の例もiTunes Apple APIを使用して音楽のアルバムを検索する。
環境
Moya & RxMoya をPodでインストール
use_frameworks! pod 'Moya/RxSwift' pod 'SwiftyJSON' # JSONに変換するのに使用
RxMoya 準備
API ターゲットを設定、ターゲットをTargetTypeプロトコルに準拠させる 方法は 前記事のMoya とほぼ同じ。違いは import Moya としている箇所を
import RxMoya
RxMoyaに変更し、enumの値に対応したHTTPメソッドを指定 している箇所のMoya.Methodを
internal var method: RxMoya.Method { // 略 }
RxMoyaに変更する。
リクエスト方法
MoyaProviderのリクエストをサブスクライブするとリクストが開始される。
注意として、リクエストが完了する前にオブザーバブルを破棄するとキャンセルされる。キャンセルされないようにMoyaProviderをローカルに保持する必要がある。
// providerを生成 let provider = RxMoyaProvider<iTunes>() let disposeBag = DisposeBag()
実際のリクエスト。
func requestWithRxMoya() { // 検索ターム"swift"を指定して、Search APIをリクエストする // responseの型は Observable<Response> let response = provider.request(.Search("swift")) let disposable = response .map { (response: Response) -> JSON in // SwiftyJSONを使用して、response.date (NSData型) を // JSONに変換 return JSON(data: response.data) } .subscribeNext { // results配列(アルバム情報)を出力 if let results = $0["results"].array { print(results) } else { print("Error") } } // メモリ管理 disposeBag.addDisposable(disposable) }
以下のようなアルバム情報の配列が出力される。
[{ "trackCount" : 14, "collectionId" : 907242701, "collectionName" : "1989", "artistName" : "Taylor Swift", ... },{ ... }, ...
参考URL
RxSwift 入門 その7
RxSwift 入門 その6 - タコさんブログ の続き。 今回は、RxSwiftプレイグラウンドの Mathematical and Aggregate Operators 、Connectable Observable Operators の項。プレイグラウンドの内容としてはこれで最後。
Mathematical and Aggregate Operators
この項では、Observableによって送信される全てのアイテムのシーケンス(ストリーム)に作用するオペレータに関して説明してある。
以下、プレイグラウンドに説明してあるMathematical and Aggregate Operatorsに関するオペレータ。
- concat
- reduce
concat
2つ、または、それ以上のObservablesから送信されるアイテム(イベント)を割り込みすることなく送信する。
let s1 = [0, 1, 2].toObservable() let s2 = [5, 6, 7, 8].toObservable() _ = s1.concat(s2) .subscribe { print($0) }
この出力は、
Next(0) Next(1) Next(2) Next(5) Next(6) Next(7) Next(8) Completed
このマーブルダイアグラムは以下のように表せられる。
s1 -0-1-2| s2 -5-6-7-8| ↓ concat s2 r -0-1-2-5-6-7-8|
reduce
Observableが送信する各アイテム(イベント)に関数を順次適用し、結果を送信する。reduceは、完了するまでシーケンス(ストリーム)の各要素に関数を実行し、蓄積された値をメッセージと共に送信する。これはシーケンス上のSwiftのreduce関数と同じような働きをする。
let stream = (0..<10).toObservable() _ = stream .reduce(0, accumulator: +) // 初期値 0 .subscribe { print($0) }
この出力は、
Next(45) Completed
Connectable Observable Operators
この項では、Connectable Observable に関して説明してある。Connectable Observableは、サブスクライブした時ではなく、connect()メソッドが呼ばれた時にアイテムの送信が開始されること以外は、通常のObservableに似ている。このようにして、Observableがアイテムの送信を開始する前に、Observableをサブスクライブするために、すべての期待するサブスクライバーを待つことができる。
以下、プレイグラウンドに説明してあるConnectable Observable Operatorsに関するオペレータ。
- multicast
- replay
- publish
この項を実行するにあたっての注意
この項のサンプルは、//sampleWithoutConnectableOperators() のようにコメントアウトされているので、実行を確認する場合はコメントを外す必要がある
intervalオペレータを使用して、整数のシーケンス(ストリーム)を生成している。intervalオペレータは与えられた時間間隔ごとに無限に増加する整数のアイテムを送信するシーケンスを生成する
intervalオペレータを使用しているため、 XCPlaygroundPage.currentPage.needsIndefiniteExecution = true とする必要がある。また、遅延させる必要があるため、サポート関数として以下のdelay関数を利用している
// delay秒後に、メインスレッドでclosureを実行する public func delay(delay:Double, closure:()->()) { dispatch_after( dispatch_time(DISPATCH_TIME_NOW,Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure) }
Without Connect
connectを使用しない場合の、挙動確認。
// 1秒間隔で整数を送信するシーケンスを生成する let s = Observable<Int>.interval(1, scheduler: MainScheduler.instance) // 1つ目のサブスクライブ _ = s .subscribe { print("first subscription \($0)") } // 2秒後に、さらにサブスクライブする delay(2) { print("-- after delay(2) --") _ = s .subscribe { print("second subscription \($0)") } }
この出力は、
first subscription Next(0) first subscription Next(1) -- after delay(2) -- first subscription Next(2) second subscription Next(0) first subscription Next(3) second subscription Next(1) first subscription Next(4) // ...
after delay(2) 後の first subscription の値が 2 で、second subscription の値は 0 になっていることが分かる。
multicast
let subject = PublishSubject<Int>() _ = subject .subscribe { print("Subject \($0)") } // multicast を使用して Connectable Observable を取得する let s = Observable<Int>.interval(1, scheduler: MainScheduler.instance) .multicast(subject) _ = s .subscribe { print("first subscription \($0)") } delay(2) { print("-- connect 呼びだし --") s.connect() } delay(4) { print("-- after delay(4) --") _ = s .subscribe { print("second subscription \($0)") } } delay(6) { print("-- after delay(6) --") _ = s .subscribe { print("third subscription \($0)") } }
この出力は、
-- connect 呼びだし -- Subject Next(0) first subscription Next(0) -- after delay(4) -- Subject Next(1) first subscription Next(1) second subscription Next(1) Subject Next(2) first subscription Next(2) second subscription Next(2) -- after delay(6) -- Subject Next(3) first subscription Next(3) second subscription Next(3) third subscription Next(3) Subject Next(4) first subscription Next(4) second subscription Next(4) third subscription Next(4) // ...
connect呼びだし後にアイテム(イベント)が送信され、first・second・third subscriptionの値が同じになっていることが分かる。
replay
replayオペレータは、Observableがアイテムの送信を開始した後に、オブザーバがサブスクライブしても、全てのオブザーバが同じ送信されたアイテム(イベント)のシーケンス(ストリーム)を受け取ることを保証する。
publish = multicast + replay subject
let s = Observable<Int>.interval(1, scheduler: MainScheduler.instance) .replay(1) // Buffer Size:1 _ = s .subscribe { print("first subscription \($0)") } delay(2) { print("-- connect 呼びだし --") s.connect() } delay(4) { print("-- after delay(4) --") _ = s .subscribe { print("second subscription \($0)") } } delay(6) { print("-- after delay(6) --") _ = s .subscribe { print("third subscription \($0)") } }
この出力は、
-- connect 呼びだし -- first subscription Next(0) -- after delay(4) -- second subscription Next(0) first subscription Next(1) second subscription Next(1) first subscription Next(2) second subscription Next(2) -- after delay(6) -- third subscription Next(2) first subscription Next(3) second subscription Next(3) third subscription Next(3) first subscription Next(4) second subscription Next(4) third subscription Next(4) // ...
publish
通常のObservableをConnectable Observableへ変換する。
publish = multicast + publish subject
つまり、publishは基本的にはreplay(0)と同じ。
let s = Observable<Int>.interval(1, scheduler: MainScheduler.instance) .publish() _ = s .subscribe { print("first subscription \($0)") } delay(2) { print("-- connect 呼びだし --") s.connect() } delay(4) { print("-- after delay(4) --") _ = s .subscribe { print("second subscription \($0)") } } delay(6) { print("-- after delay(5) --") _ = s .subscribe { print("third subscription \($0)") } }
この出力は、
-- connect 呼びだし -- first subscription Next(0) -- after delay(4) -- first subscription Next(1) second subscription Next(1) first subscription Next(2) second subscription Next(2) -- after delay(5) -- first subscription Next(3) second subscription Next(3) third subscription Next(3) first subscription Next(4) second subscription Next(4) third subscription Next(4) // ...
参考URL
Mathematical and Aggregate Operators
Connectable Observable Operators
RxSwift 入門 その6
RxSwift 入門 その5 - タコさんブログ の続き。
今回は、RxSwiftプレイグラウンドの Error Handling Operators 、Observable Utility Operators 、 Conditional and Boolean Operators の項。
Error Handling Operators
この項では、Observableからのエラー通知から復帰するのに役立つオペレータに関して説明してある。
以下、プレイグラウンドに説明してあるError Handlingに関するオペレータ。
- catchError
- retry
catchError
catchErrorオペレータは、エラーなしでシーケンスを継続することによってエラー通知から復帰する。
let s = PublishSubject<Int>() _ = s .catchError { error in return [10,11,12].toObservable() } .subscribe { print($0) } s.on(.Next(1)) s.on(.Next(2)) s.on(.Next(3)) s.on(.Error(NSError(domain: "Test", code: 0, userInfo: nil)))
この出力は、
Next(1) Next(2) Next(3) Next(10) Next(11) Next(12) Completed
このマーブルダイアグラムは以下のように表せられる。
-1-2-3-x -10-11-12| ↓ 結果 -1-2-3-1-1-1| 0-1-2|
retry
retryオペレータは、ソースObservableがエラーを送信したら、再度サブスクライブする。
以下の例は、bad practice とコメントしてある通り、例を示すためだけのもの。
var counter = 1 let stream = Observable<Int>.create { observer in observer.on(.Next(0)) observer.on(.Next(1)) observer.on(.Next(2)) // エラーを故意に起こす if counter < 2 { let error = NSError(domain: "Test", code: 0, userInfo: nil) observer.on(.Error(error)) counter += 1 } observer.on(.Next(3)) observer.on(.Next(4)) observer.on(.Next(5)) observer.on(.Completed) return NopDisposable.instance } _ = stream .retry() .subscribe { print($0) }
この出力は、
Next(0) Next(1) Next(2) Next(0) Next(1) Next(2) Next(3) Next(4) Next(5) Completed
このマーブルダイアグラムは以下のように表せられる。
-0-1-2-x -0-1-2-3-4-5| ↓ 結果 -0-1-2-0-1-2-3-4-5|
Observable Utility Operators
この項では、Observableを操作するのに役立つオペレータ、subscribeオペレータとわずかに異なるオペレータに関して説明してある。
以下、この項で説明してあるオペレータ。
- subscribe
- subscribeNext
- subscribeCompleted
- subscribeError
- doOn
subscribe
おなじみのsubscribeオペレータ。
subscribeオペレータはobserverとobservableを結びつけるのりとなるオペレータ。
おなじみなので例は省略する。
subscribeNext, subscribeCompleted
subscribeNextは、Observableにエレメントハンドラをサブスクライブするオペレータ。Observableがアイテム(イベント)を送信するときに呼ばれる。
subscribeCompletedは、Observableにコンプリーションハンドラをサブスクライブするオペレータ。エラーが起こっていない場合、Observableが最後のアイテムを送信した後にコンプリーションハンドラが呼ばれる。
let stream = PublishSubject<Int>() // Nextをサブスクライブ _ = stream .subscribeNext { print($0) } // Completedをサブスクライブ _ = stream .subscribeCompleted { print("Completed") } stream.on(.Next(1)) stream.on(.Completed)
この出力は、
1 Completed
subscribeError
subscribeErrorは、Observableにエラーハンドラをサブスクライブするオペレータ。
let stream = PublishSubject<Int>() _ = stream .subscribeError { error in print("Error:", error) } let error = NSError(domain: "Examples", code: -1, userInfo: nil) stream.on(.Error(error))
この出力は、
Error: Error Domain=Examples Code=-1 "(null)"
doOn
doOnオペレータは、いろいろなObservableのライフサイクルイベント(Next, Complete, Error)に応じるためにアクションを登録する。
let stream = (0...6).toObservable() _ = stream .doOn { print("インターセプトされたイベント \($0)") }.filter { $0 % 2 == 0 }.subscribe { print($0) }
この出力は、
インターセプトされたイベント Next(0) Next(0) インターセプトされたイベント Next(1) インターセプトされたイベント Next(2) Next(2) インターセプトされたイベント Next(3) インターセプトされたイベント Next(4) Next(4) インターセプトされたイベント Next(5) インターセプトされたイベント Next(6) Next(6) インターセプトされたイベント Completed Completed
Conditional and Boolean Operators
この項では、いくつかのObservablesによって送信される1つ以上のObservables、または、アイテム(イベント)を評価するオペレータに関して説明してある。
以下、この項で説明してあるオペレータ。
- takeUntil
- takeWhile
takeUntil
takeUntilは、2番目のObservableがアイテムを送信、または終了した後に、一番目のObservableから送信されたアイテムを破棄する。
let s1 = PublishSubject<Int>() let s2 = PublishSubject<Int>() _ = s1 .takeUntil(s2) .subscribe { print($0) } s1.on(.Next(1)) s1.on(.Next(2)) s1.on(.Next(3)) s1.on(.Next(4)) s2.on(.Next(1)) s1.on(.Next(5))
この出力は、
Next(1) Next(2) Next(3) Next(4) Completed
このマーブルダイアグラムは以下のように表せられる。
s1 -1-2-3-4---5- s2 -1-- ↓ takeUntil s2 r -1-2-3-4-|
takeWhile
takeWhileは、指定された条件が偽となるまでObservableから送信されたアイテム(イベント)を反映する。
let stream = (0..<10).toObservable() _ = stream .takeWhile { x in x < 4 } .subscribe { print($0) }
この出力は、
Next(0) Next(1) Next(2) Next(3) Completed
このマーブルダイアグラムは以下のように表せられる。
-0-1-2-3-4-5-6-7-8-9| ↓ takeWhile { x < 4 } -0-1-2-3|
参考URL
Error Handling operators
Observable Utility Operators
Conditional and Boolean Operators