この記事はiOS Advent Calendar 2016の5日目の記事です。
Why
数日前、Qiitaのタイムラインにこんなものが流れてきました。
タグを見たところまだ手を付けている人も少なく、AdventCalendarのネタにも困っていた僕は「これだ!」と思ってSwiftを書くことにしました。当然今となっては何人か同じことをやっている人もいると思いますがとりあえずお付き合いください笑
Gistはコチラ
Code
コードはこちらです。
//: Playground - PPAP: http://qiita.com/KIchiro/items/74d117ff894ac10b72e0 のSwift版
import UIKit
import Foundation
import AVFoundation
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
// enumの一覧を使いたい
// http://qiita.com/su_k/items/c1c6a91324cf12151256
public protocol EnumEnumerable {
associatedtype Case = Self
}
public extension EnumEnumerable where Case: Hashable {
private static var iterator: AnyIterator<Case> {
var n = 0
return AnyIterator {
defer { n += 1 }
let next = withUnsafePointer(to: &n) {
UnsafeRawPointer($0).assumingMemoryBound(to: Case.self).pointee
}
return next.hashValue == n ? next : nil
}
}
public static func enumerate() -> EnumeratedSequence<AnySequence<Case>> {
return AnySequence(self.iterator).enumerated()
}
public static var cases: [Case] {
return Array(self.iterator)
}
public static var count: Int {
return self.cases.count
}
}
// 列挙体
// http://qiita.com/hachinobu/items/392c96820588d1c03b0c
fileprivate enum Word: String, EnumEnumerable {
case pen
case pineapple
case apple
}
fileprivate enum PpapState {
case initial, p, pp, ppa, ppap
func transition(_ next: Word) -> PpapState {
switch (next, self) {
case (.pen, .initial): return .p
case (.pineapple, .p): return .pp
case (.apple, .pp): return .ppa
case (.pen, .ppa): return .ppap
default: return .initial
}
}
}
// 喋らせる
// http://qiita.com/takecian/items/096b07e2d0437454f670
let talker = AVSpeechSynthesizer()
func ppapMachine() {
var state: PpapState = .initial
while state != .ppap {
let word = Word.cases[Int(arc4random_uniform(UInt32(Word.count)))]
state = state.transition(word)
speakWord(generateSentence(word))
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))
talker.stopSpeaking(at: .word)
}
speakWord("Apple pen")
RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))
talker.stopSpeaking(at: .word)
speakWord("Pineapple pen")
RunLoop.main.run(until: Date(timeIntervalSinceNow: 3.5))
talker.stopSpeaking(at: .word)
speakWord("Uh!")
RunLoop.main.run(until: Date(timeIntervalSinceNow: 1))
talker.stopSpeaking(at: .word)
speakWord("Pen pineapple apple pen!")
}
fileprivate func generateSentence(_ word: Word) -> String {
switch word {
case .apple:
return "I have an \(word.rawValue)"
default:
return "I have a \(word.rawValue)"
}
}
func speakWord(_ string: String) {
let utterance = AVSpeechUtterance(string: string)
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
talker.speak(utterance)
}
ppapMachine()
こだわり
まずロジック自体に関しては元ネタを参考にさせていただきました。その上でせっかくのiOSなので喋らせてみることにしました。
喋らせる上でループが早すぎて再生されなくなるのでRunLoopをつかって一秒遅延させています。またより簡潔に書くためにenumの一覧を取得できるProtocolExtensionを使わせていただきました。
Learning
やはりenumのprotocol extensionが普段使わないハッシュ値やポインターをバンバン活用してて学ぶところが大きかったです。
完全に理解しきっているわけではないのですが、普段使わないものを使うだけでこんなにプログラミングの幅が広がるということ、さらにC/C++のような低レイヤチックなことを扱うのにもSwiftがつかえるんだなと実感できました。
最後に軽く自分なりのそれぞれの解釈をリストにしておこうと思います。
-
defer
... これは有名。スコープを抜ける直前に実行する -
AnyIterator
... C++の::iteratorと同じに見える。多分渡したクロージャーの返り値をどんどん生成していってくれるのかな -
UnsafeRawPointer($0).assumingMemoryBound(to: Case.self).pointee
... おそらくではあるけど、UnsafeRawPointer
で$0
のポインタを生成して、そこからCase
のサイズ分、ここではWord
のサイズ分ずらしたところのポインタをpointee
で取っているのかな...? -
withUnsafePointer
... 少し解釈が難しいけど、上の内容で推測するとtoに渡したポインタを基準に新たなポインタを生成するということかと -
hashValue
の比較 ... これはhashValue
Swiftで検索してわかったけどどうやらenum
が列挙した順に最小ハッシュ値を自動で割り振ってくれているらしい。つまりcaseの定義した順に0,1,2,...と割り振られていく。つまりnext.hashValueがnになっていればnextがn+1番目に定義されている要素になるという寸法か -
AnySequence
... これはわかりやすい。AnyIterator
を受け取ってそっから列を生成するんでしょう。で、enumurate()
はそれにインデックスとペアの列を生成するみたいな
こんなところでしょうか。後はArray
がイテレーターで初期化できるというのはかなり便利なのではないかなと感じました。
Result
最後のPen pineapple apple pen!は聞くことができなかったのか、それともそこに到達する前にプログラムが止まっちゃうのかよくわかりませんが確認することができませんでした汗
もしかしたら文法めちゃくちゃだから喋ってくれなかった...?(そんなわけないか)
追記(2016/12/05 20:24:39)
今までのコードだと喋る前に実行が終わってしまってPen pineapple apple pen!が喋られていないことが発覚したので、それを防ぐためにココを参考に
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
このコードを追記しました。またそれにともなって、一回ごとにtalkerをストップすることでタイミングを調整し、最後には本家PPAPと同じように終わるように変更しました。
感想
Playgroundを使えば音声合成とかアニメーションとか、簡単にできてしまうのがSwiftの、iOSの強みだよなぁなんて思いました。また、なにかバズりそうなネタがあればSwiftで参加してみたいです笑