AVSpeechSynthesizerをバックグラウンド再生ありで使う

(2019/08/09更新。バックグラウンド移行時に音が止まっているならAudioSessionを無効化する方法に書き換えました。)

以前[iOS]電話がかかってきたとき、ヘッドホンジャックが抜かれたときの対応 – nackpan Blogという記事を書きました。

そこでのkkさんのコメントをきっかけにAVSpeechSynthesizerの使い方について調べました。

AVSpeechSynthesizerを用いてテキストを読み上げるアプリ。フォアグラウンドで再生している場合は問題ありません。
バックグラウンド再生を絡めた場合にトラブル。
AVSpeechSynthesizerでspeak中にpauseしたあとアプリを中断、再びアプリを開くと勝手に音が鳴るなど、アプリ再開時の挙動が妙でした。

ログを見るとアプリ再開時にinterruptionの通知が届いていたので、interruptionNotificationについて調査。

InterruptionNotificationについて

Interruptionの通知は、電話がかかってきた時などでAudioSessionに割り込みが発生した時に届くものと考えていました。
今回、アプリの再開時に、interruptionの通知が届きました。

Appleのドキュメントを見直しました。

interruptionNotification – AVAudioSession | Apple Developer Documentation

Noteとして以下の記述があります。

Starting in iOS 10, the system will deactivate the audio session of most apps in response to the app process being suspended. When the app starts running again, it will receive an interruption notification that its audio session has been deactivated by the system. This notification is necessarily delayed in time because it can only be delivered once the app is running again. If your app’s audio session was suspended for this reason, the userInfo dictionary will contain the AVAudioSessionInterruptionWasSuspendedKey key with a value of true.

If your audio session is configured to be non-mixable (the default behavior for the playback, playAndRecord, soloAmbient, and multiRoute categories), it’s recommended that you deactivate your audio session if you’re not actively using audio when you go into the background. Doing so will avoid having your audio session deactivated by the system (and receiving this somewhat confusing notification).

interruptionNotification – AVAudioSession | Apple Developer Documentation

 * iOS 10以降では、システムは、アプリプロセスが中断されると、ほとんどのアプリの AudioSessionを無効にする。
*  アプリの再開時に、AudioSessionがシステムによって無効にされたことを示す割り込み(interruption)通知を受け取る。
* この通知でAVAudioSessionInterruptionWasSuspendedKeyの値はtrue
* AudioSessionがミックス不可(playback、playAndRecord、soloAmbient、multiRouteのデフォルトの動作)設定なら、バックグラウンド移行時に、アクティブに使用していないAudioSessionを無効にすることをお勧め
 * そうすることで、この通知を受け取らなくて済む

とあるように、システムによってAudioSessionが無効にされたことを示すinterruptionがアプリの再開時に届く、とのことでした。

InterruptionNotificationの実験

バックグラウンド再生ありのAVSpeechSynthesizerを使ったアプリで、interruptionの通知について実験しました。
当初、実験した際(07/29)には、iOS 12.4で
* AVSpeechSynthesizerでspeak中に、pause。電源ボタンを押してアプリを中断。すると音声が勝手に鳴ってしまう。電源ボタンを押した直後にinterruptionの通知が届きました。(willResignActiveの前に発生)
という症状が発生しました。
しかし、その後あらためて実験すると、まったくこの症状は出ず、interruptionNotification – AVAudioSession のドキュメントにあるように再開後にinterruptionの通知が届きました。

対処法

アプリ再開時に音が勝手になるなど挙動が変になる問題は、システムによるAudioSessionの無効化が関係しているのだろう。AVSpeechSynthesizerでは再開時に上手いことやってくれない。Noteにあるように、明示的にAudioSessionを無効化すれば、通知を受け取らずに済むし挙動も適切なものになると考えました。

interruptionNotificationのNoteでは、アプリのAudioSessionが無効にされたことを示す通知がアプリ再開後に届くとあり、それを避けるには、バックグラウンド移行時にAudioSessionを無効化するとよい、とありました。


当初の実験(07/29)では、iOS 12.4での電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、バックグラウンド移行時にAudioSessionを無効化する処理を書いても、音声が短いながらも勝手に鳴ってしまう症状が出ました。そのため、AVSpeechSynthesizerでの音声再生が終了したら、その段階でAudioSessionを無効化にするということで対処することにしていました。これにはAVSpeechSynthesizerDelegateを使いAVSpeechSynthesizerのpause, stop, finishの完了を知り、そこでAudioSessionを無効化にしていました。

しかし、その後の実験では「電源ボタンによるアプリの中断で、電源ボタンを押した直後に通知が届き、音声が短いながらも勝手に鳴ってしまう」症状は再現しませんでした。


そこで、interruptionNotificationのNoteが勧める方法に従いました。
バックグラウンド移行時に音が止まっている場合、AudioSessionを無効化。
speakのさいにAudioSessionを有効化しました。

バックグラウンド移行を検知する方法はこちらの記事が参考になります。
NotificationCenterを用いたライフサイクルイベントの検知

実装

今回はViewController.swiftにすべて記述することとしました。
メッセージラベル、Speakボタン、Pauseボタン、Stopボタンを貼り付けます。

アプリ画面

メッセージラベルとViewControllerをOutletで結びます。
@IBOutlet weak var messageLabel: UILabel!
SpeakボタンとViewControllerをActionで結びます。
@IBAction func speakBtnTapped(_ sender: Any) 
PauseボタンとViewControllerをActionで結びます。
@IBAction func pauseBtnTapped(_ sender: Any)
StopボタンとViewControllerをActionで結びます。
@IBAction func stopBtnTapped(_ sender: Any)

バックグラウンド再生用。
Capabilities > Background Modes をON > Audio, AirPlay, and Picture in Pictureにチェック

ViewController.swift

import UIKit
import AVFoundation
import AVKit
import MediaPlayer

class ViewController: UIViewController, AVSpeechSynthesizerDelegate {

    @IBOutlet weak var messageLabel: UILabel!
    var syntherizer = AVSpeechSynthesizer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // AudioSessionカテゴリをbackground再生ができるものに設定
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback)
        } catch {
            print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }

        addAudioSessionObservers()
        addRemoteCommandEvent()
        addLifeCycleObserver()
    }
    
    // 電話による割り込みと、オーディオルートの変化を監視します
    func addAudioSessionObservers() {
        let center = NotificationCenter.default
        center.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
        center.addObserver(self, selector: #selector(audioSessionRouteChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
    }
    
    /// Interruption : 電話などによる割り込み
    @objc func handleInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
            let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                return
        }
        
        if type == .began {
            // interruptionが開始した時(電話がかかってきたなど)
            if let wasSuspendedKeyValue = userInfo[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber {
                let wasSuspendedKey = wasSuspendedKeyValue.boolValue
                if wasSuspendedKey {
                    // suspeended key : true
                } else {
                    // suspended key : false
                }
            } else {
                // suspended key : nil
            }
        }
        else if type == .ended {
            // interruptionが終了した時の処理
            if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
                let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
                if options.contains(.shouldResume) {
                    // Interruption Ended - playback should resume
                } else {
                    // Interruption Ended - playback should NOT resume
                }
            }
        }
    }
    
    /// Audio Session Route Change : ルートが変化した(ヘッドフォンが抜き差しされた)
    @objc func audioSessionRouteChanged(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
            let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
            let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
                return
        }
        
        DispatchQueue.main.async {
            self.messageLabel.text = self.routeChangeReasonDescription(reason: reason)
        }

        switch reason {
        case .newDeviceAvailable:
            let session = AVAudioSession.sharedInstance()
            for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                // ヘッドフォンがつながった
                
                break
            }
        case .oldDeviceUnavailable:
            if let previousRoute =
                userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
                for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                    // ヘッドフォンが外れた
                    
                    // 音声をpauseしています
                    pause()
                    break
                }
            }
        default: ()
        }
        
    }
    
    public func speak() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(true)
        } catch {
            
        }
        let voice = AVSpeechSynthesisVoice(language: "ja-JP")
        let utterance = AVSpeechUtterance(string: "おはようございます。今日もいい天気ですね。音声合成で読み上げています。文章をうまくよめていますか?")
        utterance.voice = voice
        utterance.preUtteranceDelay = 5
        
        if syntherizer.isPaused {
            syntherizer.continueSpeaking()
        } else {
            syntherizer.speak(utterance)
        }
    }
    
    public func pause() {
        syntherizer.pauseSpeaking(at: .immediate)
    }
    
    /// Speakボタンが押された
    @IBAction func speakBtnTapped(_ sender: Any) {
        // 音声出力を行う
        speak()
    }
    
    /// pauseボタンが押された
    @IBAction func pauseBtnTapped(_ sender: Any) {
        pause()
    }
    
    @IBAction func stopBtnTapped(_ sender: Any) {
        syntherizer.stopSpeaking(at: .immediate)
    }
    
    /// 監視する必要がなくなった段階で、Observerを取り外します
    private func removeAudioSessionObservers() {
        let center = NotificationCenter.default
        
        // AVAudio Session
        center.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
        center.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
        
    }
    
    /// route変更の理由表示用
    private func routeChangeReasonDescription(reason: AVAudioSession.RouteChangeReason) -> String {
        switch reason {
        case .unknown:
            return "unknown"
        case .newDeviceAvailable:
            return "newDeviceAvailable"
        case .oldDeviceUnavailable:
            return "oldDeviceUnailable"
        case .categoryChange:
            return "categoryChange"
        case .override:
            return "override"
        case .wakeFromSleep:
            return "wakeFromSleep"
        case .noSuitableRouteForCategory:
            return "noSuitableRouteForCategory"
        case .routeConfigurationChange:
            return "routeConfigurationChange"
        default:
            return "default"
        }
    }
    
    // MARK: Remote Command Event
    func addRemoteCommandEvent() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(type(of: self).remoteTogglePlayPause(_:)))
        commandCenter.playCommand.addTarget(self, action: #selector(type(of: self).remotePlay(_:)))
        commandCenter.pauseCommand.addTarget(self, action: #selector(type(of: self).remotePause(_:)))
        commandCenter.nextTrackCommand.addTarget(self, action: #selector(type(of: self).remoteNextTrack(_:)))
        commandCenter.previousTrackCommand.addTarget(self, action: #selector(type(of: self).remotePrevTrack(_:)))
    }
    
    @objc func remoteTogglePlayPause(_ event: MPRemoteCommandEvent) {
        // イヤホンのセンターボタンを押した時の処理
        // 略

    }
    
    @objc func remotePlay(_ event: MPRemoteCommandEvent) {
        // プレイボタンが押された時の処理
        speak()

    }
    
    @objc func remotePause(_ event: MPRemoteCommandEvent) {
        // ポーズボタンが押された時の処理
        pause()
        
    }
    
    @objc func remoteNextTrack(_ event: MPRemoteCommandEvent) {
        // 「次へ」ボタンが押された時の処理
        // (略)
    }
    
    @objc func remotePrevTrack(_ event: MPRemoteCommandEvent) {
        // 「前へ」ボタンが押された時の処理
        // (略)
        
    }
    
    // MARK: - Life Cycle
    func addLifeCycleObserver() {
        let center = NotificationCenter.default
        // 今回はbackground移行を検知
        center.addObserver(self, selector: #selector(didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
    }
    
    @objc func didEnterBackground(_ notification: Notification) {
        // 音が止まっていたらAudioSessionを無効化
        if !syntherizer.isSpeaking || syntherizer.isPaused {
            deactivateAudioSession()
        }

        let app = UIApplication.shared
        app.beginBackgroundTask(expirationHandler: {
            // このblockはバックグラウンド処理に入って、所定時間後(180秒程度後)に実行される
            // (バックグラウンド再生が継続している間は実行されない)
            // ここでAudioSessionを無効化しておく
            // (この処理がないとバックグラウンド再生中にpauseしたあと、3分すぎにアプリを再開すると
            // いきなり音が鳴る症状が出てしまう)
            self.deactivateAudioSession()
            
        })
    }
    
    func deactivateAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            
        }
    }
}

CoreGraphicsコードをSwift 3.0に変換する

Xcode 8.0が登場しました。
旧プロジェクトをXcode 8.0で開くと、Swift 3.0(あるいはSwift 2.4)に変換するよう促すダイアログが出現しました。

CoreGraphicsコードをSwift 3.0への自動コンバートにかけたところ、変換されない部分が残りました。

        let pathRef = CGMutablePath()
        CGPathMoveToPoint(pathRef, nil, 20, 0)
        CGPathAddLineToPoint(pathRef, nil, 200, 0)
        CGPathAddCurveToPoint(pathRef, nil, 205.523, 0, 210, 4.435, 210, 10)
        CGPathAddLineToPoint(pathRef, nil, 210, 122)
        CGPathAddCurveToPoint(pathRef, nil, 210, 127.565, 205.523, 132, 200, 132)
        CGPathAddLineToPoint(pathRef, nil, 10, 132)
        CGPathAddCurveToPoint(pathRef, nil, 4.477, 132, -0, 127.565, -0, 122)
        CGPathAddLineToPoint(pathRef, nil, -0, 20)
        CGPathAddCurveToPoint(pathRef, nil, -0, 9.087, 9.081, 0, 20, 0)
        pathRef.closeSubpath()

変換後、このようなコードになったのですが最終的には

        let pathRef = CGMutablePath()
        pathRef.move(to: CGPoint(x: 20, y: 0))
        pathRef.addLine(to: CGPoint(x: 200, y: 0))
        pathRef.addCurve(to: CGPoint(x: 210, y: 10), control1: CGPoint(x: 205.523, y: 0), control2: CGPoint(x: 210, y: 4.435))
        pathRef.addLine(to: CGPoint(x: 210, y: 122))
        pathRef.addCurve(to: CGPoint(x: 200, y: 132), control1: CGPoint(x: 210, y: 127.565), control2: CGPoint(x: 205.523, y: 132))
        pathRef.addLine(to: CGPoint(x: 10, y: 132))
        pathRef.addCurve(to: CGPoint(x: -0, y: 122), control1: CGPoint(x: 4.477, y: 132), control2: CGPoint(x: -0, y: 127.565))
        pathRef.addLine(to: CGPoint(x: -0, y: 20))
        pathRef.addCurve(to: CGPoint(x: 20, y: 0), control1: CGPoint(x: -0, y: 9.087), control2: CGPoint(x: 9.081, y: 0))
        pathRef.closeSubpath()

と変換したい。

文字列の処理がやりやすいのはなにかしらと考えて、急遽Rubyを学ぶことにしました。


文字列操作とファイル操作の基本的なやりかたを知ったので、以下の変換プログラム converter.rbを書きました。
CGPathMoveToPoint(pathRef, nil, 20, 0)

pathRef.move(to: CGPoint(x: 20, y: 0))

CGPathAddLineToPoint(pathRef, nil, 200, 0)

pathRef.addLine(to: CGPoint(x: 200, y: 0))

CGPathAddCurveToPoint(pathRef, nil, 205.523, 0, 210, 4.435, 210, 10)

pathRef.addCurve(to: CGPoint(x: 210, y: 10), control1: CGPoint(x: 205.523, y: 0), control2: CGPoint(x: 210, y: 4.435))

と変換するものです。

converter.rb

class Converter

    def convert()
        loop do 
            print "Filename? "
            fileName = gets.chomp
            
            if fileName == ""
                break
            end
            
            # Backup
            from = fileName
            to = "_" + fileName + ".bak"
            copy(from, to)
            
            # Conversion
            array = []
            File.open(fileName) do |file|
                file.each_line do |line|
                    line = convertCoreGraphicsCode(line)
                    array.push(line)
                end
            end
                
            # Writing
            File.open(fileName, "w") do |file|
                file.puts(array)
                puts fileName + " 変換終了"
            end
        end
    end
    
    def copy(from, to)
        File.open(from) do |input|
            File.open(to, "w") do |output|
                output.write(input.read)
            end
        end
    end

    def convertCoreGraphicsCode(line)
        if line.include?("CGPathMoveToPoint")
            return convertMoveToPoint(line)
        elsif line.include?("CGPathAddLineToPoint") 
            return convertAddLineToPoint(line)
        elsif line.include?("CGPathAddCurveToPoint")
            return convertAddCurveToPoint(line)
        else
            return line
        end
    end
    
    # CGPathMoveToPoint(clipPath, nil, 240, 122)
    # to
    # clipPath.move(to: CGPoint(x: 240, y: 122))
    def convertMoveToPoint(line)
        # indentを取得
        index = line.index("CGPathMoveToPoint")
        indent = line[0, index]
        
        # path名を含むかたまりを取得("CGPathMoveToPoint(clipPath,")
        pathStr = line.match(/CGPathMoveToPoint\(\w+,/)
        
        # path名を取得("clipPath")
        pathName = pathStr[0].sub("CGPathMoveToPoint\(", "").chop
        # puts pathName
        
        # 数値
        figuresStr = line.sub(/CGPathMoveToPoint\(\w+, nil,/, "").chomp.chop.lstrip
        figuresStr = " " + figuresStr
        figures = figuresStr.split(",")
        # puts figures
        
        dstStr = "%s%s.move(to: CGPoint(x:%s, y:%s))" % [indent, pathName, figures[0], figures[1]]
        # puts dstStr
        return dstStr
    end
    
    
    # CGPathAddLineToPoint(clipPath, nil, 240, 122)
    # to
    # clipPath.addLine(to: CGPoint(x: 240, y: 122))
    def convertAddLineToPoint(line)
        # indentを取得
        index = line.index("CGPathAddLineToPoint")
        indent = line[0, index]
        
        # path名を含むかたまりを取得("CGPathAddLineToPoint(clipPath,")
        pathStr = line.match(/CGPathAddLineToPoint\(\w+,/)
        
        # path名を取得("clipPath")
        pathName = pathStr[0].sub("CGPathAddLineToPoint\(", "").chop
        # puts pathName
        
        # 数値
        figuresStr = line.sub(/CGPathAddLineToPoint\(\w+, nil,/, "").chomp.chop.lstrip
        figuresStr = " " + figuresStr
        figures = figuresStr.split(",")
        # puts figures

        dstStr = "%s%s.addLine(to: CGPoint(x:%s, y:%s))" % [indent, pathName, figures[0], figures[1]]
        # puts dstStr
        return dstStr
    end
    
    # CGPathAddCurveToPoint(pathRef2, nil, 4.477, 132, 0, 127.565, 0, 122)
    # to
    # pathRef2.addCurve(to: CGPoint(x: 0, y: 122), control1: CGPoint(x: 4.477, y: 132), control2: CGPoint(x: 0, y: 127.565))
    def convertAddCurveToPoint(line)

        # indentを取得
        index = line.index("CGPathAddCurveToPoint")
        indent = line[0, index]
        
        # path名を含むかたまりを取得("CGPathAddCurveToPoint(pathRef2,")
        pathStr = line.match(/CGPathAddCurveToPoint\(\w+,/)
        
        # path名を取得("pathRef2")
        pathName = pathStr[0].sub("CGPathAddCurveToPoint\(", "").chop
        # puts pathName
        
        # 数値を取得
        figuresStr = line.sub(/CGPathAddCurveToPoint\(\w+, nil,/, "").chomp.chop.lstrip
        figuresStr = " " + figuresStr
        figures = figuresStr.split(",")
        # puts figures
        
        dstStr = "%s%s.addCurve(to: CGPoint(x:%s, y:%s), control1: CGPoint(x:%s, y:%s), control2: CGPoint(x:%s, y:%s))" % [indent, pathName, figures[4], figures[5], figures[0], figures[1], figures[2], figures[3]]
        # puts dstStr
        return dstStr 
    end

end

converter = Converter.new
converter.convert()

変換したいswiftファイルがあるフォルダに、converter.rbをおき、Terminalでそのフォルダへ移動した後、

ruby converter.rb  

と入力するとプログラムが実行されます。
実行すると、

FileName?  

とたずねられるので、そこで変換したいファイル名を入力すると変換が行われます。
(このさい、”_元ファイル名.bak”というバックアップファイルも作成します。)
ファイル名を入力せずにreturnキーを押すと、プログラムは終了します。

関連

Autodesk Graphic(旧iDraw)はCore Graphicsのコードを生成できる – nackpan Blog

Dropbox API v1からv2への移行

作成したiOSアプリ「Repete」(語学学習支援プレイヤー)には、Dropbox上のオーディオファイルを取り入れる機能があります。
Dropboxへのアクセスには、Dropboxが提供するAPIを使用しています。
API v1 is now deprecated | Dropbox Developer Blog
Dropbox API v1が廃止され、新たなアプリおよび既存のアプリはAPI v2を使ってDropboxと接続する必要があるとのことで、今回、移行処理を行いました。

File Type Permissionが廃止された

API v1には、File type Permissionという分類がありました。特定の種類のファイルのみアクセスできるというものです。
API v1 → API v2 migration guide – Developers – Dropbox
API v2では、File Types Permissionがなくなりました。移行にあたっては新たにアプリを登録して、ユーザーにもう一度認証してもらうことになります。

Tip: When messaging to your users the need to re-link their Dropbox account, we suggest the following language: “The Dropbox integration has changed. Please re-link your account to continue [syncing/backing up data/accessing files/etc].”

再リンクの必要を伝えるメッセージの例。

Fie Types Permissionのように特定の種類のファイルのみを取得するには、ファイル拡張子を用いて判別します。
Developer guide – Dropbox
こちらに、API v1でFile type判別に用いていた拡張子の一覧があります。

インストール

Swift製のライブラリをインストール。
Dropbox for Swift Developers | Install
こちらのページにそって、インストール。
AlamofireとSwiftyDropboxがインストールされます。
今回インストールしたバージョンは、
Alamofire: 3.3.1
SwiftyDropbox: 3.2.0

チュートリアル

Dropbox for Swift Developers | Tutorial
チュートリアルでは、アプリを登録して簡単なプロジェクトを作成しました。
チュートリアルプロジェクトでできること。
* Dropboxとの連携許可
* ユーザーのアカウント名を表示
* 指定したフォルダ直下の内容を表示
* ファイルのアップロード
* ファイルのdiskへのダウンロード
* ファイルのメモリへのダウンロード

おおよそやりたいことはできるのだけれど、そのほかに知りたい点がいくつか。
Tutorialを改造しながら、さぐりさぐりで調べていきました。

そのほか調べた事柄

ファイルの情報を知る

getMetadataを用いる。

if let client = Dropbox.authorizedClient {
    // ファイルのpathを指定して情報を取得
    // ("hello.txt"がルートフォルダにあるとします)
    client.files.getMetadata(path: "/hello.txt").response {
        response, error in
        
        print("*** get Metadata ***")
        if let metadata = response {
            print("result:\(metadata.name)")
            print("")
            
        }
    }
}

フォルダかファイルか?

取得したmetadataがフォルダなのかファイルなのか知りたい。
Files.FolderMetadata、Files.FileMetadataを用いる。

if let client = Dropbox.authorizedClient {
    // ルートフォルダ直下の内容を取得して、名前を表示。ファイルの場合はサイズも表示
    client.files.listFolder(path: "")
        .response { response, error in
            print()
            print("*** List folder ***")
            if let result = response {
                for entry in result.entries {
                    if entry is Files.FileMetadata {
                        let file = entry as! Files.FileMetadata
                        print("\(file.name) \(file.size)")
                        
                    } else {
                        print("\(entry.name)")
                    }
                }
            } else {
                print(error!)
            }
    }
}

ファイルの種類を限定

Dropbox for Swift Developers | Overview
こちらのページで紹介されているSample Appの”PhotoWatch”では、

// Check that file is a photo (by file extension)
if entry.name.hasSuffix(".jpg") || entry.name.hasSuffix(".png") {
    // Add photo!
    self.filenames?.append(entry.name)
}

という書き方をしています。
拡張子をチェックして判別しています。
参考:Developer guide – Dropbox
Dropbox API v1でFile type判別に用いていた拡張子の一覧あり。

フォルダ下のすべてのフォルダ・ファイルを取得する

client.files.listFolder(path: "", recursive: true, includeMediaInfo: false, includeDeleted: false, includeHasExplicitSharedMembers: false)

listFolderのさいに、recursiveをtrueにすることで再帰的に調べてフォルダ下のすべてのフォルダ・ファイルが取得できます。

進行状況

進行状況を知るには?

if let client = Dropbox.authorizedClient {
    let destination : (NSURL, NSHTTPURLResponse) -> NSURL = { temporaryURL, response in
        let fileManager = NSFileManager.defaultManager()
        let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
        // generate a unique name for this file in case we've seen it before
        let UUID = NSUUID().UUIDString
        let pathComponent = "\(UUID)-\(response.suggestedFilename!)"
        return directoryURL.URLByAppendingPathComponent(pathComponent)
    }
    
    // ルートフォルダにある"audio.mp3"ファイルをダウンロードする
    client.files.download(path: "/audio.mp3", destination: destination)
        .progress{ bytesRead, totalBytesRead, totalBytesExpectedToRead in
            
            print("bytesRead:\(bytesRead) totalBytesRead:\(totalBytesRead) totalBytesExpectedToRead:\(totalBytesExpectedToRead)")
            
        }
        .response { response, error in
            if let (metadata, url) = response {
                print()
                print("*** Downloaded file to disk ***")
                print("Downloaded file name: \(metadata.name)")
                print("Downloaded file url: \(url)")
            } else {
                print(error!)
            }
    }
}

参考:swift – SwiftyDropbox download progress – Stack Overflow

リクエストをキャンセルする

ダウンロードのリクエストをキャンセルするには?
swifty dropbox download cancel – Dropbox Community
こちらのページを読むと、初期はダウンロードキャンセル機能はなかったようです。
最新版ではあるとのこと。
SwiftyDropbox 3.2.0ではありました。

import UIKit
import SwiftyDropbox

class ViewController: UIViewController {
    
    var dlRequest: DownloadRequestFile<Files.FileMetadataSerializer, Files.DownloadErrorSerializer>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Verify user is logged into Dropbox
        if let client = Dropbox.authorizedClient {

            // Download a file to disk
            // ルートフォルダにある"audio.mp3"ファイルをダウンロードする。
            let destination : (NSURL, NSHTTPURLResponse) -> NSURL = { temporaryURL, response in
                let fileManager = NSFileManager.defaultManager()
                let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
                // generate a unique name for this file in case we've seen it before
                let UUID = NSUUID().UUIDString
                let pathComponent = "\(UUID)-\(response.suggestedFilename!)"
                return directoryURL.URLByAppendingPathComponent(pathComponent)
            }
            
            print("Downloading")
            dlRequest = client.files.download(path: "/audio.mp3", destination: destination).progress{ bytesRead, totalBytesRead, totalBytesExpectedToRead in
                
                    print("bytesRead:\(bytesRead) totalBytesRead:\(totalBytesRead) totalBytesExpectedToRead:\(totalBytesExpectedToRead)")

                }.response { response, error in
                    if let (metadata, url) = response {
                        print()
                        print("*** Downloaded file to disk ***")
                        print("Downloaded file name: \(metadata.name)")
                        print("Downloaded file url: \(url)")
                    } else {
                        print(error!)
                    }
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func linkButtonPressed(sender: AnyObject) {
        if Dropbox.authorizedClient == nil {
            Dropbox.authorizeFromController(self)
        }
    }

    // (storyboard上にキャンセル用ボタンを配置し、actionでつないだ)
    @IBAction func cancelButtonPressed(sender: AnyObject) {
        if let request = dlRequest {
            request.cancel()
            dlRequest = nil
        }
    }
}

こんな感じでしょうか。
Storyboard側にキャンセル用ボタンを配置してViewController.swiftのcancelButtonPressedとactionでつないであるものとします。
Dropboxにリンクしている状態で、viewDidLoadが呼ばれると、ルートフォルダの”audio.mp3″のダウンロードが開始されます。
キャンセルボタンを押すと、ダウンロードがキャンセルされます。


以上、SwiftyDropboxの使い方について、いくつか知った事柄について記しました。

Swiftでオブジェクトのアドレスを表示する

Swiftでオブジェクトのメモリアドレスを表示するには?

Objective-Cでは、

UIView* view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)];
NSLog(@"%p", view);
// 0x13d649b60

と、%pを使うことで、アドレスを知ることができた。


Swiftでは

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
print("\(unsafeAddressOf(view))")
// "0x00007fe8f8708740\n"

と、unsafeAddressOfを用いることで、アドレスを知ることができる。

[iOS][Swift]ミュージックライブラリの音楽の再生、情報の表示(MPMusicPlayerController使用)

MPMusicPlayerControllerを使ったミュージックライブラリアイテムの再生に関してこちらに記事を書きました。
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) | nackpan Blog
この記事では、ミュージックライブラリから一つの曲を選んで再生・一時停止・停止を行いました。

今回のサンプルでは、MPMusicPlayerControllerを使用して複数のアイテムを順に再生します。
また、再生中の音楽アイテムの情報(アートワーク、アルバム名等)を表示します。
前回のサンプルでは、選曲するとすぐに再生を開始していましたが、今回は選曲するとアイテム情報を表示しますが再生は開始しないこととしました。PLAYボタンを押すと再生開始です。
再生されているアイテムが変更されると、それを検知して変更後のアイテム情報を表示します。
ファイル 2016-04-13 7 24 35
(今回のサンプルのスクリーンショット)

MusicPlayerControllerを使って、ミュージックライブラリのアイテムを再生し、情報を表示する

iPodライブラリアクセス プログラミングガイド(PDF)
iPod Library Access Programming Guide(英語版)
このappleのドキュメントにライブラリにアクセスして再生する方法、必要な曲を選択する方法、現在再生中の音楽の情報を知る方法等、まとめてあります。
(ちなみに、日本語ドキュメント – Apple DeveloperのページにappleのiOS用日本語ドキュメントがまとめてあります。英語版へのリンクもあります)

iOSシミュレータでは動作しないので、実機を用いてください。

Single View Applicationでプロジェクトを作成。
ss 2015-09-16 6.46.53

ViewController.SwiftにMediaPlayerフレームワークをimportします。

import UIKit
import MediaPlayer

class ViewController: UIViewController {

UI配置

ボタンとラベルとイメージビュー(Image View)を配置。
ss 2016-04-13 2.02.06

UIとViewControllerとの接続

ラベルおよびイメージビューとViewController.swiftをOutletを作成して接続。
ss 2016-04-12 21.53.43outletLabels
ss 2016-04-12 21.52.55

ボタンとViewController.swiftをactionを作成して接続。
ss 2016-04-13 2.35.53Action
ss 2015-09-13 9.09.50

プレイヤー準備

プレイヤーを表すpropertyをViewController.swiftに加えます。

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var artistLabel: UILabel!
    @IBOutlet weak var albumLabel: UILabel!
    @IBOutlet weak var songLabel: UILabel!

    var player = MPMusicPlayerController()
    
    override func viewDidLoad() {

プレイヤーのインスタンスを作成。

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
    }

(ここで、applicationMusicPlayerではなく、systemMusicPlayerを用いると、「ミュージック」アプリでの再生状況(再生アイテムや、シャッフル、リピートなどのモード)を反映したものになる)

再生中アイテムの変更通知

今回は、複数の曲を選択して順に再生していきます。
アイテム情報を表示するには、現在再生中のアイテムがなにか分かっていなければなりません。
ミュージックプレーヤー通知という仕組みで、再生中アイテムに変更があった場合には通知を受け取ることができます。
この通知を受けて、プレイヤーが現在再生中のアイテムを取得し情報表示を更新します。

まず、再生中アイテム変更イベントを監視します。

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
        // 再生中のItemが変わった時に通知を受け取る
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // 通知の有効化
        player.beginGeneratingPlaybackNotifications()
        
    }

再生中のアイテムが変更になったときに、ViewController.nowPlayingItemChanged(_:)を呼び出すように指定しています。

また、ViewControllerのdeinit内に、通知を受け取る必要がなくなった場合の後処理を書いておきます。

    deinit {
        // 再生中アイテム変更に対する監視をはずす
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.removeObserver(self, name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // ミュージックプレーヤー通知の無効化  
        player.endGeneratingPlaybackNotifications()
    }

再生中のアイテムが変更になった際に呼び出されるViewController.nowPlayingItemChanged(_:)の内容を書きます。

    /// 再生中の曲が変更になったときに呼ばれる  
    func nowPlayingItemChanged(notification: NSNotification) {
        
        if let mediaItem = player.nowPlayingItem {
            updateSongInformationUI(mediaItem)
        }
        
    }

MPMusicPlayerControllerのプロパティnowPlayingItemで再生中のアイテムを取得できます。
(今回は使いませんが、アイテムをsetすることもできます)
nowPlayingItemはMPMediaItem型のプロパティです。

MPMediaItem

ミュージックライブラリに対するアクセスでは、MPMediaItemというクラスでアイテム情報を扱います。
MPMediaItem Class Reference
MPMediaItemは、title, artist, artworkなどなど、さまざまな情報をもっています。
アイテム情報を表示する際には、現在再生中のアイテムから情報を取得してラベルやイメージビューに表示します。

    /// 曲情報を表示する
    func updateSongInformationUI(mediaItem: MPMediaItem) {
    
        // 曲情報表示
        // (a ?? b は、a != nil ? a! : b を示す演算子です)  
        // (aがnilの場合にはbとなります)
        artistLabel.text = mediaItem.artist ?? "不明なアーティスト"
        albumLabel.text = mediaItem.albumTitle ?? "不明なアルバム"
        songLabel.text = mediaItem.title ?? "不明な曲"
        
        // アートワーク表示
        if let artwork = mediaItem.artwork {
            let image = artwork.imageWithSize(imageView.bounds.size)
            imageView.image = image
        } else {
            // アートワークがないとき
            // (今回は灰色表示としました)
            imageView.image = nil
            imageView.backgroundColor = UIColor.grayColor()
        }
        
    }

メディアアイテムピッカー

曲を選択するために、メディアアイテムピッカーを用います。
(メディアアイテムピッカーというのは、iOSで用意されているあらかじめ設定済みのViewController。ミュージックライブラリの選択画面と同じようなことが出来る)
ss 2015-09-13 11 41 39

メディアアイテムピッカーでの「選択完了したとき」や「キャンセルされたとき」のイベントを、ViewControllerで受け取れるようにします。
そのために、ViewControllerをメディアアイテムピッカーのデリゲートとして設定します。
「選曲」ボタンを押すと、メディアアイテムピッカーを作成して、デリゲートの設定を行い、ライブラリの曲を選択できるようにします。
「選択完了したとき」「キャンセルされたとき」のメソッドを記述します。

import UIKit
import MediaPlayer

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    @IBAction func pick(sender: AnyObject) {
        // MPMediaPickerControllerのインスタンスを作成
        let picker = MPMediaPickerController()
        // ピッカーのデリゲートを設定
        picker.delegate = self
        // 複数選択にする。(falseにすると、単数選択になる)
        picker.allowsPickingMultipleItems = true
        // ピッカーを表示する
        presentViewController(picker, animated: true, completion: nil)
        
    }
    
    /// メディアアイテムピッカーでアイテムを選択完了したときに呼び出される
    func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        // プレイヤーを止める
        player.stop()
        
        // 選択した曲情報がmediaItemCollectionに入っているので、これをplayerにセット。
        player.setQueueWithItemCollection(mediaItemCollection)
        
        // 選択した曲から最初の曲の情報を表示
        if let mediaItem = mediaItemCollection.items.first {
            updateSongInformationUI(mediaItem)
        }
        
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
        
    }
    
    //選択がキャンセルされた場合に呼ばれる
    func mediaPickerDidCancel(mediaPicker: MPMediaPickerController) {
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
    }

選択完了した時に呼び出されるmediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection)で、MPMediaItemCollection型の引数mediaItemCollectionがありますが、これにアイテム情報が入っています。
MPMediaItemCollection Class Reference
MPMediaItemCollectionクラスは、MPMediaItemを集めて管理しているクラスです。
そのプロパティitemsがMPMediaItemの配列になっていますので、今回はそこから先頭のMPMediaItemを取得してそれを元に情報を表示しています。

再生・一時停止・停止

各ボタンのアクションに、「再生」「一時停止」「停止」機能を加えます。

    @IBAction func pushPlay(sender: AnyObject) {
        player.play()
    }

    @IBAction func pushPause(sender: AnyObject) {
        player.pause()
    }

    @IBAction func pushStop(sender: AnyObject) {
        player.stop()
    }

iOSシミュレータでは動作しません。実機を用いてください。 「選曲」ボタンを押すと、メディアアイテムピッカーが表示されるので、曲を選択してください。複数の曲を選択できます。「PLAY」ボタンで音楽の再生。「PAUSE」ボタンで一時停止。「STOP」ボタンで音楽を止めて、再生位置を一番始めに戻します。


ViewController.swift全文

//
//  ViewController.swift
//  MPMusicPlayerControllerDemo2
//
//  Created by KUWAJIMA MITSURU on 2016/04/12.
//  Copyright © 2016年 nackpan. All rights reserved.
//

import UIKit
import MediaPlayer

class ViewController: UIViewController, MPMediaPickerControllerDelegate {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var artistLabel: UILabel!
    @IBOutlet weak var albumLabel: UILabel!
    @IBOutlet weak var songLabel: UILabel!

    var player = MPMusicPlayerController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        player = MPMusicPlayerController.applicationMusicPlayer()
        //player = MPMusicPlayerController.systemMusicPlayer()
        
        // 再生中のItemが変わった時に通知を受け取る
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector: #selector(ViewController.nowPlayingItemChanged(_:)), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // 通知の有効化
        player.beginGeneratingPlaybackNotifications()
        
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func pick(sender: AnyObject) {
        // MPMediaPickerControllerのインスタンスを作成
        let picker = MPMediaPickerController()
        // ピッカーのデリゲートを設定
        picker.delegate = self
        // 複数選択にする。(falseにすると、単数選択になる)
        picker.allowsPickingMultipleItems = true
        // ピッカーを表示する
        presentViewController(picker, animated: true, completion: nil)
        
    }
    
    /// メディアアイテムピッカーでアイテムを選択完了したときに呼び出される
    func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
        
        // プレイヤーを止める
        player.stop()
        
        // 選択した曲情報がmediaItemCollectionに入っているので、これをplayerにセット。
        player.setQueueWithItemCollection(mediaItemCollection)
        
        // 選択した曲から最初の曲の情報を表示
        if let mediaItem = mediaItemCollection.items.first {
            updateSongInformationUI(mediaItem)
        }
        
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
        
    }

    
    /// 選択がキャンセルされた場合に呼ばれる
    func mediaPickerDidCancel(mediaPicker: MPMediaPickerController) {
        // ピッカーを閉じ、破棄する
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    /// 曲情報を表示する
    func updateSongInformationUI(mediaItem: MPMediaItem) {
    
        // 曲情報表示
        // (a ?? b は、a != nil ? a! : b を示す演算子です)  
        // (aがnilの場合にはbとなります)
        artistLabel.text = mediaItem.artist ?? "不明なアーティスト"
        albumLabel.text = mediaItem.albumTitle ?? "不明なアルバム"
        songLabel.text = mediaItem.title ?? "不明な曲"
        
        // アートワーク表示
        if let artwork = mediaItem.artwork {
            let image = artwork.imageWithSize(imageView.bounds.size)
            imageView.image = image
        } else {
            // アートワークがないとき
            // (今回は灰色表示としました)
            imageView.image = nil
            imageView.backgroundColor = UIColor.grayColor()
        }
        
    }
    

    
    
    @IBAction func pushPlay(sender: AnyObject) {
        player.play()
    }
    
    @IBAction func pushPause(sender: AnyObject) {
        player.pause()
    }
    
    @IBAction func pushStop(sender: AnyObject) {
        player.stop()
    }
    
    
    /// 再生中の曲が変更になったときに呼ばれる  
    func nowPlayingItemChanged(notification: NSNotification) {
        
        if let mediaItem = player.nowPlayingItem {
            updateSongInformationUI(mediaItem)
        }
        
    }
    
    deinit {
        // 再生中アイテム変更に対する監視をはずす
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.removeObserver(self, name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: player)
        // ミュージックプレーヤー通知の無効化  
        player.endGeneratingPlaybackNotifications()
    }
    

}

関連

[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(MPMusicPlayerController使用) | nackpan Blog
[iOS][Swift]MPMediaQueryを使って曲を絞り込む | nackpan Blog
[iOS][Swift]ミュージックライブラリにアクセスして音楽を再生する(AVAudioPlayer使用) | nackpan Blog

Autodesk Graphic(旧iDraw)はCore Graphicsのコードを生成できる

Autodesk Graphic(旧iDraw)は、図形からCore Graphicsのコードを書き出せることを先日知ったので、記します。

言語を選択

環境設定 > 読み込み / 書き出し
で、SwiftかObjective-Cかを選択
ss 2016-02-07 8.05.12

クリップボードへ書き出し

必要なレイヤーを選択して、Ctrl+クリック(もしくは編集メニュー)から、
別名でコピー > Core Graphics Codeを選択。
ss 2016-02-14 22.19.30
これでクリッブボードにコードがコピーされています。
ペースト(Ctrl + Vなど)すると、以下のようなコードが貼り付けられます。

#if os(iOS)
    let ctx = UIGraphicsGetCurrentContext() // iOS
#else
    let contextPtr = NSGraphicsContext.currentContext()!.graphicsPort   // OS X
    let opaqueCtx = COpaquePointer(contextPtr)
    let ctx = Unmanaged<CGContext>.fromOpaque(opaqueCtx).takeUnretainedValue()
#endif

// enable the following lines for flipped coordinate systems
// CGContextTranslateCTM(ctx, 0, self.bounds.size.height)
// CGContextScaleCTM(ctx, 1, -1)
let colorSpace = CGColorSpaceCreateDeviceRGB()

let scaleFactor: CGFloat = 1;
// CGContextScaleCTM(ctx, scaleFactor, scaleFactor);

/*  Shape   */
let pathRef = CGPathCreateMutable()
CGPathMoveToPoint(pathRef, nil, 10, -0)
CGPathAddLineToPoint(pathRef, nil, 230, -0)
CGPathAddCurveToPoint(pathRef, nil, 235.523, -0, 240, 4.477, 240, 10)
CGPathAddLineToPoint(pathRef, nil, 240, 121)
CGPathAddCurveToPoint(pathRef, nil, 240, 126.523, 235.523, 131, 230, 131)
CGPathAddLineToPoint(pathRef, nil, 10, 131)
CGPathAddCurveToPoint(pathRef, nil, 4.477, 131, 0, 126.523, 0, 121)
CGPathAddLineToPoint(pathRef, nil, 0, 10)
CGPathAddCurveToPoint(pathRef, nil, 0, 4.477, 4.477, -0, 10, -0)
CGPathCloseSubpath(pathRef)

// (以下略)

省略してしまいましたが、インナーシャドー、グラデーション、輪郭線を用いた角丸四角形と三角形を描くのに130行程度のコードになっています。

生成されたコードからUIImageを作成

生成されたコードでは、CurrentContextを用いて、そこに描画しています。
サイズを指定してContextを作成し、それをCurrentContextとするのには
UIGraphicsBeginImageContextWithOptionsが使えます。

// サイズを指定してContextを作成し、それをCurrentContextとする
// (sizeはCGSize型の変数)  
UIGraphicsBeginImageContextWithOptions(size, false, 0)

// (ここにAutodesk Graphicsが生成したコード)

// CurrentContextからUIImage?の作成  
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()

// このContextをスタックから取り除く(CurrentContextではなくなる)
UIGraphicsEndImageContext()


こんなかんじで、UIImageに持ってくることができます。

スケールについて
let scaleFactor: CGFloat = 1;
// CGContextScaleCTM(ctx, scaleFactor, scaleFactor);

生成されたコードではコメントアウトされているCGContextScaleCTMを生かしてscaleFactorを変えれば、スケールの変更ができます。

一部書き方があっていないところあり

Autodesk GraphicのVer 3.0.1を使用しているのですが、一部、書き方がSwift 2.1に適合していない部分がありました。
グラデーションのOption指定部分を修正しました。

// 修正前
CGContextDrawLinearGradient(ctx, gradientRef, CGPoint(x: 130.334, y: 82.171), CGPoint(x: 130.334, y: 44.818), CGGradientDrawingOptions(kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation))
// 修正後
CGContextDrawLinearGradient(ctx, gradientRef, CGPoint(x: 130.334, y: 82.171), CGPoint(x: 130.334, y: 44.818), [.DrawsBeforeStartLocation, .DrawsAfterEndLocation])
テキスト

テキスト部分を書き出してみるとCoreTextを使用したコードで書き出されました。
Path化されてしまうのではなく、テキスト情報が残っています。
ss 2016-02-14 22.54.08

#if os(iOS)
    let ctx = UIGraphicsGetCurrentContext() // iOS
#else
    let contextPtr = NSGraphicsContext.currentContext()!.graphicsPort   // OS X
    let opaqueCtx = COpaquePointer(contextPtr)
    let ctx = Unmanaged<CGContext>.fromOpaque(opaqueCtx).takeUnretainedValue()
#endif

// enable the following lines for flipped coordinate systems
// CGContextTranslateCTM(ctx, 0, self.bounds.size.height)
// CGContextScaleCTM(ctx, 1, -1)
let colorSpace = CGColorSpaceCreateDeviceRGB()

let scaleFactor: CGFloat = 1;
// CGContextScaleCTM(ctx, scaleFactor, scaleFactor);

/*  Text   */
let textBox = CGRect(x: 13.912, y: 5.804, width: 65.76, height: 38)
let textStr: CFString = "ABC"

let attributedStr = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0)
CFAttributedStringReplaceString(attributedStr, CFRange(location: 0, length: 0), textStr)

let fontRef = CTFontCreateWithName("HelveticaNeue", 32, nil)
let textRange = CFRange(location: 0, length: CFAttributedStringGetLength(attributedStr))
CFAttributedStringSetAttribute(attributedStr, textRange, kCTFontAttributeName, fontRef)

let textColorComps: [CGFloat] = [0.953, 0.388, 0.388, 1]
let textColor = CGColorCreate(colorSpace, textColorComps)
CFAttributedStringSetAttribute(attributedStr, textRange, kCTForegroundColorAttributeName, textColor)

var alignment = CTTextAlignment.TextAlignmentCenter
var paragraphSettings = CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.Alignment, valueSize: UInt(sizeof(UInt8)), value: &alignment)
let paragraphStyle = CTParagraphStyleCreate(&paragraphSettings, 1)
CFAttributedStringSetAttribute(attributedStr, textRange, kCTParagraphStyleAttributeName, paragraphStyle)

let textBoxPath = CGPathCreateWithRect(CGRect(x: 0, y: 0, width: textBox.size.width, height: textBox.size.height), nil)
let framesetter = CTFramesetterCreateWithAttributedString(attributedStr)
let frameRef = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: 0), textBoxPath, nil)
CGContextSaveGState(ctx)
CGContextTranslateCTM(ctx, textBox.origin.x, textBox.origin.y)

CGContextSetTextMatrix(ctx, CGAffineTransformIdentity)
CGContextTranslateCTM(ctx, 0.0, textBox.size.height)
CGContextScaleCTM(ctx, 1.0, -1.0)
CTFrameDraw(frameRef, ctx)

CGContextRestoreGState(ctx)

一部でエラーが出たので修正しました

// 修正前
var alignment = CTTextAlignment.TextAlignmentCenter
var paragraphSettings = CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.Alignment, valueSize: UInt(sizeof(UInt8)), value: &alignment)
// (略)
CTFrameDraw(frameRef, ctx)
// 修正後
var alignment = CTTextAlignment.Center
var paragraphSettings = CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.Alignment, valueSize: Int(sizeof(UInt8)), value: &alignment)
// (略)
CTFrameDraw(frameRef, ctx!)

ということで、Autodesk GraphicはCore Graphics Codeを書き出せます。

関連

CoreGraphicsコードをSwift 3.0に変換する – nackpan Blog

[Swift]指定した要素を配列から探してそのインデックスを取得

指定した要素を配列から探してそのインデックスを取得

indexOfを用いる
(Swift 1系で用いられていたfindはなくなりました)

Stringの配列

let strs = ["aiueo", "ABC", "いろは"]

let index = strs.indexOf("ABC")
// index = 1

指定した要素がなかった場合は、nilが返ってくる

配列の要素がクラスのインスタンスの場合

class RItem {
    let title: String
    let duration: Float
    
    init(title: String, duration: Float) {
        self.title = title
        self.duration = duration
    }
    
}

let item0 = RItem(title: "A-Album", duration: 13.0)
let item1 = RItem(title: "B-Album", duration: 4.9)
let item2 = RItem(title: "C-Album", duration: 8.9)

let items = [item0, item1, item2]

if let index = items.indexOf({$0 === item1}) {
    print(index)
}

同一インスタンスの参照であるかを判定するには、===を用いる
(The Swift Programming Language (Swift 2.1): Classes and Structures)

[Swift]iPhoneを振動させる

iPhoneのバイブレーション機能を用いるには?

システムサウンドにバイブレーションさせるものがあるのでそれを用います。

System Sound Services Reference

• AudioToolboxをimportしたうえで、バイブレーション用サウンドを指定して鳴らすことで、振動させることができます。

import AudioToolbox
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)

バイブレーションのサンプル

Startボタンを押すと3秒間隔で振動し続けるサンプルを作成しました。(Stopボタンを押すと止まります)

Single View Applicationで開発。

Main.storyboardにStartボタンを配置
ss 2015-11-13 17.09.33

StartボタンとViewController.swift間をActionで結びつける

    @IBAction func pushStartOrStopBtn(sender: AnyObject) {

    }

Startボタンが押されると、Timerが起動します。
Timerは3秒間隔で、システムサウンドによるバイブレーションを実行しています。
また、ボタンは押されるごとに、”Start”と”Stop”の表記を切り替えています。
Stopボタンが押されると、Timerを無効にします。

ViewController.swift

//
//  ViewController.swift
//  Vibration Demo
//
//  Created by KUWAJIMA MITSURU on 2015/11/13.
//  Copyright © 2015年 nackpan. All rights reserved.
//

import UIKit
import AudioToolbox

class ViewController: UIViewController {
    
    var timer = NSTimer()
    var nowPlaying = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    @IBAction func pushStartOrStopBtn(sender: AnyObject) {
        let btn = sender as! UIButton
        if nowPlaying {
            pushStopBtn(btn)
        } else {
            pushStartBtn(btn)
            
        }
    }
    
    func pushStartBtn(btn: UIButton) {
        timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: "vibrate:", userInfo: nil, repeats: true)
        timer.fire()
        
        
        btn.setTitle("Stop", forState: UIControlState.Normal)
        
        nowPlaying = true
        
    }

    
    func vibrate(timer: NSTimer) {
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
    }
    
    func pushStopBtn(btn: UIButton) {
        
        timer.invalidate()
        
        btn.setTitle("Start", forState: UIControlState.Normal)
        
        nowPlaying = false
        
    }
}