LoginSignup
8
4

More than 5 years have passed since last update.

SwiftでPPAP

Last updated at Posted at 2016-12-04

この記事はiOS Advent Calendar 2016の5日目の記事です。

Why

数日前、Qiitaのタイムラインにこんなものが流れてきました。

[Javaの小枝] JavaでPPAP

タグを見たところまだ手を付けている人も少なく、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で参加してみたいです笑

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4