タコさんブログ

プログラミングメモと小言

Swift で Audio Queue Services を使って再生する

Audio Queue Services Programming Guide - Playing Audio を参考にSwiftでAudio Queue Serviceを使用する。

環境

準備

AudioToolbox をインポートする。

import AudioToolbox

Audio Queue Service を使用する手順

  1. 状態・フォーマット・パス情報を管理する構造体を定義する
  2. 実際の再生を行うオーディオキューコールバックを作成する
  3. オーディオキューバッファのサイズを決定するコードを書く
  4. プレイバックするオーディオファイルを開き、オーディオデータのフォーマットを取得する
  5. プレイバックオーディオキューを作成し、プレイバックに設定する
  6. オーディオキューバッファを生成し、エンキューする
  7. リソースの解放

状態・フォーマット・パス情報を管理するクラス(構造体)を定義

オーディオフォーマットとオーディオキューの情報を管理するクラスを定義する。 状態の更新を行うのでクラス(参照型)で定義する。

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つのことをする

  1. 指定された分のデータをオーディオファイルから読み込み、それをオーディオキューバッファに入れる
  2. バッファキューにオーディオバッファキューをエンキューする
  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 でヒマワリを描く

bitterharvest.hatenablog.com

に触発されて、試しに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等すれば完成。以下のようなひまわりが描画される。

f:id:tiny_wing:20160331190347p:plain

注意

試す場合は、Playgroundでは時間がかかるので、Single View Application で行う方が良い(?)

参考URL

Swift 型消去 (Type Erasure)

try! Swiftで発表のあった型消去の話。
この例では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 OperatorsConnectable 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

この項を実行するにあたっての注意

  1. この項のサンプルは、//sampleWithoutConnectableOperators() のようにコメントアウトされているので、実行を確認する場合はコメントを外す必要がある

  2. intervalオペレータを使用して、整数のシーケンス(ストリーム)を生成している。intervalオペレータは与えられた時間間隔ごとに無限に増加する整数のアイテムを送信するシーケンスを生成する

  3. 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 OperatorsObservable Utility OperatorsConditional 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