20191222のiOSに関する記事は17件です。

AVAudioEngineでリモートの音楽ファイルを再生する

やりたいこと

リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいのでAVAudioEngineを使う。
XambmPaQ.png

環境

手順

  1. 音楽ファイルをダウンロードする
  2. パケットに分割する
  3. パケットをPCMに変換する
  4. PCMをAVAudioEngineを使って再生する

実装

step1. 音楽ファイルをダウンロードする

URLSessionを使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるようにRemoteAudioDownloaderのprotocolを定義

RemoteAudioDownloader.swift
public protocol RemoteAudioDownloader {
    func request(url: URL, completionHandler: @escaping (_ data: Data) -> Void)
}

internal class DefaultRemoteAudioDownloader: RemoteAudioDownloader {
    var task: URLSessionDataTask?
    var completionHandler: ((_ data: Data) -> Void)?
    func request(url: URL, completionHandler: @escaping (Data) -> Void) {
        self.completionHandler = completionHandler
        let request = URLRequest(url: url)
        task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) in
            guard let self = self, let data = data else { return }
            self.completionHandler?(data)
        })
        task?.resume()
    }
}

step2. パケットに分割する

step2-1. AudioFileStreamOpenを使ってDataを開く

ダウンロードしたDataをAudio File Stream Servicesでパケットに分割する。
最初にDataをAudioFileStreamOpenを使って開く。
分割したパケットはAudioFileStreamServiceで処理する。
propertyの処理にはAudioFileStreamService.propertyListenerProcedureが呼ばれる。
packetのdataの処理にはAudioFileStreamService.packetsProcedureが呼ばれる。
inClientDataのポインタとして自信のポインタを渡す。
AudioFileStreamService.propertyListenerProcedure, AudioFileStreamService.packetsProcedure内でinClientDataをAudioFileStreamServiceにキャストすることで、自信を呼び出すことができる。

AudioFileStreamService.swift
public class AudioFileStreamService {
    private var streamID: AudioFileStreamID?
    public init() {
        let inClientData = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        _ = AudioFileStreamOpen(inClientData,
                                { AudioFileStreamService.propertyListenerProcedure($0, $1, $2, $3) },
                                { AudioFileStreamService.packetsProcedure($0, $1, $2, $3, $4) },
                                kAudioFileMP3Type,
                                &streamID)
    }
}

step2-2. AudioFileStream_PropertyListenerProcに届いたpropertyを処理する

AudioFileStream_PropertyListenerProcとして指定したpropertyListenerProcedure内でAVAudioFormatを取得する。

AudioFileStreamService.swift
extension AudioFileStreamService {
    static func propertyListenerProcedure(_ inClientData: UnsafeMutableRawPointer, _ inAudioFileStream: AudioFileStreamID, _ inPropertyID: AudioFileStreamPropertyID, _ ioFlags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        switch inPropertyID {
        case kAudioFileStreamProperty_DataFormat:
            var description = AudioStreamBasicDescription()
            var propSize: UInt32 = 0
            _ = AudioFileStreamGetPropertyInfo(inAudioFileStream, inPropertyID, &propSize, nil)
            _ = AudioFileStreamGetProperty(inAudioFileStream, inPropertyID, &propSize, &description)
            print("format: ", AVAudioFormat(streamDescription: &description))
        default:
            print("unknown propertyID \(inPropertyID)")
        }
    }
}

step2-3. AudioFileStream_PacketsProcに届いたpacketsを処理する

AudioFileStream_PacketsProcとして指定したpacketsProcedure内でpacketのDataとAudioStreamPacketDescriptionを取得する。

AudioFileStreamService.swift
extension AudioFileStreamService {
    static func packetsProcedure(_ inClientData: UnsafeMutableRawPointer, _ inNumberBytes: UInt32, _ inNumberPackets: UInt32, _ inInputData: UnsafeRawPointer, _ inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        var packets: [(data: Data, description: AudioStreamPacketDescription)] = []
        let packetDescriptionList = Array(UnsafeBufferPointer(start: inPacketDescriptions, count: Int(inNumberPackets)))
        for i in 0 ..< Int(inNumberPackets) {
            let packetDescription = packetDescriptionList[i]
            let startOffset = Int(packetDescription.mStartOffset)
            let byteSize = Int(packetDescription.mDataByteSize)
            let packetData = Data(bytes: inInputData.advanced(by: startOffset), count: byteSize)
            packets.append((data: packetData, description: packetDescription))
        }
        print("packets: ", packets)
    }
}

step3. パケットをPCMに変換する

AVAudioConverterを使ってAVAudioCompressedBufferからAVAudioPCMBufferに変換する。

step3-1. DataからAVAudioCompressedBufferを生成する

AVAudioCompressedBufferを作ってDataをコピーする。

CompressedBufferConverter.swift
let compBuff = AVAudioCompressedBuffer(format: srcFormat, packetCapacity: 1, maximumPacketSize: Int(data.count))
_ = data.withUnsafeBytes({ (ptr: UnsafeRawBufferPointer) in
    memcpy(compBuff.data, ptr.baseAddress!, data.count)
})
compBuff.packetDescriptions?.pointee = AudioStreamPacketDescription(mStartOffset: 0, mVariableFramesInPacket: 0, mDataByteSize: UInt32(data.count))
compBuff.packetCount = 1
compBuff.byteLength = UInt32(data.count)

step3-2. AVAudioPCMBufferを生成する

出力用のAVAudioPCMBufferを作成する。

CompressedBufferConverter.swift
let pcmBuff = AVAudioPCMBuffer(pcmFormat: dstFormat, frameCapacity: frames)!
pcmBuff.frameLength = pcmBuff.frameCapacity

step3-3. AVAudioConverterを使って変換する

AVAudioConverterを使って変換する。

CompressedBufferConverter.swift
converter.convert(to: pcmBuff, error: &error, withInputFrom: { [weak self] (count: AVAudioPacketCount, input: UnsafeMutablePointer<AVAudioConverterInputStatus>) -> AVAudioBuffer? in
    input.pointee = .haveData
    let buff = self.audioCompressedBuffer[self.index]
    self?.index += 1
    return buff
})

step4. PCMをAVAudioEngineを使って再生する

ViewController.swift
        engine.attach(player)
        engine.connect(player, to: engine.mainMixerNode, format: internalFormat)
        engine.prepare()
        try! engine.start()
        player.play()
        if let ret = converter?.read(frames: frameCountPerRead) {
            self.scheduleBuffer(ret)
        }

さいごに

Audio File Stream Servicesは古いAPIなので使いにくいと感じました。
mp3は最初の数百フレームが無音として出力されるので、正しく出力できているのか、鳴らしてみるまで疑心暗鬼でしたw

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】複数のフラグを管理する場合に Set<Enum> を使う

この記事は

複数のフラグを管理する場合、データサイズを固定長にする目的で OptionSet を使うことがあります。
しかしフラグの数が少なく、データサイズを気にしない状況であれば、OptionSet のかわりに Set<Enum> を使うことで実装を少しシンプルにできます。

データ型の定義

OptionSet

OptionSet はビット集合を表現したプロトコルです。
true | false )を整数型の各ビットの( 1 | 0 )に対応させることで、複数のフラグを1つの 整数型で表現します。

struct ShippingOptions: OptionSet {
    let rawValue: Int

    static let nextDay   = ShippingOptions(rawValue: 1 << 0)
    static let secondDay = ShippingOptions(rawValue: 1 << 1)
    static let priority  = ShippingOptions(rawValue: 1 << 2)
    static let standard  = ShippingOptions(rawValue: 1 << 3)
}

Set<Enum>

Set の要素である列挙体はシンプルに次のように定義するだけです。

enum ShippingOptions {
    case nextDay
    case secondDay
    case priority
    case standard
}

Set<Enum> は列挙型の値を要素をもつ集合です。集合内の要素の有無によってフラグを表現します。
OptionSet の場合とは異なり、要素をビットに格納する方法を用いないので、raw-value-style-enum である必要はありません。もちろん、raw-value-style-enum でも良いです。
rawValue の型は何でも良く、Int である必要もありません。

全体集合

OptionSet では全体集合を自前で定義する必要がありますが、
列挙型であれば CaseIterable で自動合成可能です。

OptionSet

struct ShippingOptions: OptionSet {
    let rawValue: Int

    static let nextDay   = ShippingOptions(rawValue: 1 << 0)
    static let secondDay = ShippingOptions(rawValue: 1 << 1)
    static let priority  = ShippingOptions(rawValue: 1 << 2)
    static let standard  = ShippingOptions(rawValue: 1 << 3)

    /// 全体集合
    static let all: ShippingOptions = [nextDay, secondDay, priority, standard]
}

Set<Enum>

enum ShippingOptions: CaseIterable {
    case nextDay
    case secondDay
    case priority
    case standard
}

allCases から Set を作ることで全体集合になります。

let universe = Set(ShippingOptions.allCases)
// true
universe.isSuperset(of: [.nextDay, .secondDay])

使い方

OptionSetSet<Enum> はともに SetAlgebra に適合しているので、どちらも集合演算を使用することができます。フラグの設定・取得はほとんど同じ操作です。

OptionSet

var options = ShippingOptions()
options.formUnion([.nextDay, .priority])

if (options == [.nextDay, .priority]) {
    print("お急ぎ")
}

Set<Enum>

var options = Set<ShippingOptions>()
options.formUnion([.nextDay, .priority])

if (options == [.nextDay, .priority]) {
    print("お急ぎ")
}

複数のフラグを OptionSet で定義する理由

Set<Enum> の方がシンプルに定義できたりと優位性があるように見えますが、UIKit などのフレームワークでは OptionSet で定義されています。

Swift で作った Set<Enum> では Objective-C にブリッジすることができないからです。
しかし、Objective-C で作った NS_OPTIONS であれば OptionSet な構造体として Swift にブリッジすることができます。

typedef NS_OPTIONS(NSInteger, ShippingOptions) {
    ShippingOptionsNextDay   = 1 << 0,
    ShippingOptionsSecondDay = 1 << 1,
    ShippingOptionsPriority  = 1 << 2,
    ShippingOptionsStandard  = 1 << 3,

    ShippingOptionsAll = 0b1111,
};

Objective-C で上記の定義をすることで、Swift では次のようにインポートされます。

public struct ShippingOptions: OptionSet {
    public init(rawValue: Int)

    public static var nextDay:   ShippingOptions { get }
    public static var secondDay: ShippingOptions { get }
    public static var priority:  ShippingOptions { get }
    public static var standard:  ShippingOptions { get }

    public static var all: ShippingOptions { get }
}

Objective-C と Swift の両方をサポートする場合は、NS_OPTIONS で定義することになります。

まとめ

  • OptionSet は複数のフラグを固定長データに詰め込むことができる
  • Set でも OptionSet と同じ操作ができる
  • Set で実装した方がシンプル
  • Objective-C と Swift の両方をサポートしたい場合は NS_OPTIONS で実装する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SkyWayでiOS/Androidアプリを作るときの勘所

メリークリスマス :santa: (まだ早い)

SkyWayでアプリを作ろう

iOS/AndroidのSDKが提供されているので作れはしますが、抑えるところ抑えておかないとユースケースによってはいくつか困るケースが出てきます。最近質問されたのでまとめておきます。

各種音声の入出力先のハンドリング

  • Line
  • Bluetooth
  • デバイス

のハンドリングを自分でする必要があります。また抜き先に関しても同様。
iOSであればAVAudioSessionが握っている、AndroidであればAudioDeviceInfoでとれるので、必要な入力元と出力先の検知と切り替えは自前で実装しましょう。

この際にAndroidでは isWiredHeadsetOnのようなメソッドは非推奨。自前でハンドリングするためAudioSessionの管理には注意しましょう。

バックグラウンドでの処理

Androidは端末によりますが、iOSではUIBackgroundModes - audioを許可してください。
バックグラウンドへの遷移時はどちらもカメラは無効になるためremoteへは黒い画面が表示されます。
上記設定をしておけば音声はつなぎ続けることができます。

エラーハンドリング

https://webrtc.ecl.ntt.com/en/ios-reference/a00053_source.html

     SKW_PEER_ERR_NO_ERROR = 0,
     SKW_PEER_ERR_BROWSER_INCOMPATIBLE = -1,
     SKW_PEER_ERR_DISCONNECTED = -2,
     SKW_PEER_ERR_INVALID_ID = -3,
     SKW_PEER_ERR_INVALID_KEY = -4,
     SKW_PEER_ERR_NETWORK = -5,
     SKW_PEER_ERR_PEER_UNAVAILABLE = -6,
     SKW_PEER_ERR_SSL_UNAVAILABLE = -7,
     SKW_PEER_ERR_SERVER_ERROR = -8,
     SKW_PEER_ERR_SOCKET_ERROR = -9,
     SKW_PEER_ERR_SOCKET_CLOSED = -10,
     SKW_PEER_ERR_UNAVAILABLE_ID = -11,
     SKW_PEER_ERR_AUTHENTICATION = -12,
     SKW_PEER_ERR_WEBRTC = -20,
     SKW_PEER_ERR_ROOM_ERROR = -30,
     SKW_PEER_ERR_UNKNOWN = - 9999,

Peerイベントだけでもこれだけあり、なおかつ頻度としてもwarningレベルでリトライすればいいケースも多いです。websocketが動く関係上、リトライの概念は必須ですがユースケースによるので最初はエラーきたらとりあえずリトライかけておきましょう。

フロントのエラーイベントは運用上解析のために保存しておきたいですが、このエラーは結構な頻度で投げられてしまうので、何も考えずにストアしていくとだいぶなボリュームになるので注意。

ネットワークの切断・切り替え

これはちょっと調べきれてないですが、接続時間とかが課金系と関わるユースケースなら考慮が必要で、wifi/3Gの切り替えみたいのが起きるケースは自分でハンドリングするのがベターです。

両OSでそれぞれ接続が3GかWifiかくらいはとれるので、自前でやること。

相手のストリームを確認してカメラかどうか見る

これはAPIがないので、自前でやるのが良いです。
真っ黒になるのでpixelを見ましょう。

サポートデバイス

どうしても限界はあります。iOSでは問題ないですが、やはりAndroidでよくわからない動きをすることがあり・・・全部の網羅は厳しいかと思います。AndroidだとCベースでのライブラリ(*.so)を乗せる必要がありABIやCPUアーキテクチャに突っ込む必要があったりします。
CPUアーキテクチャはスマタブ等で確認することができます。
まぁ全体の90%くらいは大丈夫な印象です。最新版のSDKのサポートは5系以上です。

電池と通信量

これはもう食います。なおかつ通信方式(p2p, sft)にも依存するし、そもそも並行して何を立ち上げているかなどなど、外部要因が多いので明記しないほうが良いと思います。

callOptionで帯域指定だけでも入れておきましょう。

まとめ

マジですぐ作れるけど、マジで安定運用まではWebRTCといろんなユースケースと向き合う必要があるので気合い入れていこうな。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITableViewCellのボタンをどのCellのものか判別する

自分へのリマインドを主として書いています。
分かりにくかったら申し訳ないです。

UITableViewCellにボタンを追加

  • addTargetを追加
sample.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
       let button = UIButton()
       button.addTarget(self, action: #selector(self.buttonEvent(_: )), for: UIControl.Event.touchUpInside)

       return cell
}

- タップされたときのメソッド

sample.swift
 @objc func buttonEvent(_ sender: UIButton) {
        print("tapped.")
    }

tapされたボタンのCellを判定する

今回はbuttonにタグを設定する方法を紹介します。
- buttonにtagを追加
UIButtonにはInt型のtagをつけることができます、これをcellを追加する際にボタンに設定しておくことでボタンがどのセルのボタンなのかを判別できるようになります。

sample.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
       let button = UIButton()
       button.addTarget(self, action: #selector(self.buttonEvent(_: )), for: UIControl.Event.touchUpInside)
       //タグの設定
       cell.button.tag = indexPath.row

       return cell
}
  • メソッドの書き換え
sample.swift
 @objc func buttonEvent(_ sender: UIButton) {
        print("tapped: \([sender.tag])番目のcell"
 }

Reference

CustomCellのボタンをどのCellのものか判別する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動的解析ツール frida

この記事は執筆途中です

リバースエンジニアリングやCTFをする際に有用なツールであるfridaについてまとめた記事です。

今回は脱獄したiOSデバイスでの使用法を例に挙げながら説明します。

随時更新していけたらなと思っています。コメントにて「xxについて説明してほしい」や「xxのやり方を説明してほしい」等の要望ありましたらそちらも対応しようと思います。

fridaとは

動的解析ツールです。特徴としては

  • javascriptを用いたスクリプティングができること
  • マルチプラットフォームで動作すること(win, macOS, iOS, android etc...)

等が挙げられます。類似したツールにcycriptなどがあります。

インストール方法

pythonのバージョンは3系を使っていることが前提です。

pip install frida

使い方

fridaをインストールすると幾つかのコマンドが使えるようになります。その中でも最もよく使うのが以下の3つのツールです。

コマンド名 用途
frida fridaの対話型インターフェースを起動するコマンド
frida_trace 関数呼び出しをトレースするコマンド
frida-ps デバイスで動いているプロセスを確認するコマンド

流れとしてはfrida-psでプロセス名、またはpidを確認し、frida-tracefridaで解析していきます。

ここからは実際にFLEXingという脱獄Tweak(脱獄することで入れられる機能やアプリのことをTweakと呼びます)がどのような動作をしているか、fridaを駆使して解明していこうと思います!

その前にFLEXingについて軽く説明します。

FLEXingとは

FLEXというiOS用のデバッグツールを、任意のiOSアプリケーション上で起動できるようにするTweakです。

仕組みは簡単で、任意のアプリケーションにlibFLEX.dylib(FLEXの本体)というダイナミックライブラリをinject(注入)するだけです。

画像. 時計アプリにinjectされたFLEX

flex.jpg

実際にやってみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vision.frameworkでカメラ画像のテキスト認識を行う

前回の記事では、 Vision.framework をつかって顔認識を行いました。
今度はテキスト認識をやってみます。
ちなみに、テキストの文字認識はiOS13からの機能みたいです。

概要

カメラ画像からテキストを検出し、テキスト部分に矩形を表示。
さらにその部分に検出したテキストを出力します。

現在のところ、対応言語が英語のみのようです。
また、今回のサンプルでは端末を横にしないと、文字をうまく認識しません。

試した環境

Xcode 11.3
iOS 13.2
swift 5

実行サンプル

動画なので速度を出すため、検証精度を落として確認しています。
画面をアップにするとそこそこの精度は出ていそうです。

検証に使ったサイトのURLは以下です。
https://en.wikipedia.org/wiki/Apple

コード説明

手順的には、顔認証とほぼ同じで、リクエストを VNDetectFaceRectanglesRequest から VNRecognizeTextRequest に変更します。
VNRecognizeTextRequest で画像から検出した文字情報を [VNRecognizedTextObservation] として受け取ります。
ちなみに、 VNDetectTextRectanglesRequest でも文字の矩形取得はできるのですが、こちらの場合文字情報を取得することができません。

また、リクエストにプロパティを設定することで、文字取得条件を変更できます。

recognitionLevel = 文字の取得制度設定。 fastaccurate があり、動画で検出する場合は fast が良いみたい
recognitionLanguages = 認識する言語。 現在は英語のみ。
usesLanguageCorrection = 認識した文字を自動修正する機能。 スペルミス防止などにつかえそう?

    /// 文字認識情報の配列取得 (非同期)
    private func getTextObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNRecognizedTextObservation])->())) {
        let request = VNRecognizeTextRequest { (request, error) in
            guard let results = request.results as? [VNRecognizedTextObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        request.recognitionLevel = recognitionLevel
        request.recognitionLanguages = supportedRecognitionLanguages
        request.usesLanguageCorrection = true

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

サポートしている言語一覧は以下手続きで取得。

    /// サポートしている言語リストを取得 (現在は英語のみ)
    private lazy var supportedRecognitionLanguages : [String] = {
        return (try? VNRecognizeTextRequest.supportedRecognitionLanguages(
        for: recognitionLevel,
        revision: VNRecognizeTextRequestRevision1)) ?? []
    }()

文字取得とは直接関係ありませんが、画面上に取得した文字を表示するため、
CGContext に文字を書き込んでいます。
普通に書き込むと座標系の関係で文字列が逆転してしまうみたいで、
何気にここが一番面倒くさい処理となりました・・・

   /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, text: String, context: CGContext) {

        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
        context.setFillColor(UIColor.black.withAlphaComponent(0.6).cgColor)
        context.fill(rect)

        drawText(text, rect: rect, context: context)

    }

    /// コンテキストにテキストを描画  (そのまま描画すると文字が反転するので、反転させる必要あり)
    private func drawText(_ text: String, rect: CGRect, context: CGContext) {

        context.saveGState()
        defer {
            context.restoreGState()
        }

        let transform = CGAffineTransform(scaleX: 1, y: 1)
        context.concatenate(transform)

        guard let textStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
            return
        }
        let font = UIFont.boldSystemFont(ofSize: 20)
        let textFontAttributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.white,
            NSAttributedString.Key.paragraphStyle: textStyle
        ]

        let astr = NSAttributedString(string: text, attributes: textFontAttributes)
        let setter = CTFramesetterCreateWithAttributedString(astr)
        let path = CGPath(rect: rect, transform: nil)
        let frame = CTFramesetterCreateFrame(setter, CFRange(), path, nil)

        context.textMatrix = CGAffineTransform.identity
        CTFrameDraw(frame, context)

    }

VNRecognizedTextObservation から文字や検出範囲を取得する手続きは以下部分です。
topCandidates に検出文字候補が評価が高い順に入ってるみたいなので、一番最初の物を決め打ちで取るようにしています。

        textObservations.forEach{
            let rect = getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
            let text = $0.topCandidates(1).first?.string ?? "" // topCandidates に文字列候補配列が含まれている
            self.drawRect(rect, text: text, context: newContext)
        }

コード全体

import UIKit
import AVFoundation
import Vision

class TextObservationViewController: UIViewController {

    @IBOutlet weak var previewImageView: UIImageView!

    private let avCaptureSession = AVCaptureSession()

    /// 認識制度を設定。 リアルタイム処理なので fastで
    private let recognitionLevel : VNRequestTextRecognitionLevel = .fast

    /// サポートしている言語リストを取得 (現在は英語のみ)
    private lazy var supportedRecognitionLanguages : [String] = {
        return (try? VNRecognizeTextRequest.supportedRecognitionLanguages(
        for: recognitionLevel,
        revision: VNRecognizeTextRequestRevision1)) ?? []
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCamera()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        avCaptureSession.stopRunning()
    }

    /// カメラのセットアップ
    private func setupCamera() {
        avCaptureSession.sessionPreset = .photo

        let device = AVCaptureDevice.default(for: .video)
        let input = try! AVCaptureDeviceInput(device: device!)
        avCaptureSession.addInput(input)

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: .global())

        avCaptureSession.addOutput(videoDataOutput)
        avCaptureSession.startRunning()
    }

    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, text: String, context: CGContext) {

        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
        context.setFillColor(UIColor.black.withAlphaComponent(0.6).cgColor)
        context.fill(rect)

        drawText(text, rect: rect, context: context)

    }

    /// コンテキストにテキストを描画  (そのまま描画すると文字が反転するので、反転させる必要あり)
    private func drawText(_ text: String, rect: CGRect, context: CGContext) {

        context.saveGState()
        defer {
            context.restoreGState()
        }

        let transform = CGAffineTransform(scaleX: 1, y: 1)
        context.concatenate(transform)

        guard let textStyle = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
            return
        }
        let font = UIFont.boldSystemFont(ofSize: 20)
        let textFontAttributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.white,
            NSAttributedString.Key.paragraphStyle: textStyle
        ]

        let astr = NSAttributedString(string: text, attributes: textFontAttributes)
        let setter = CTFramesetterCreateWithAttributedString(astr)
        let path = CGPath(rect: rect, transform: nil)
        let frame = CTFramesetterCreateFrame(setter, CFRange(), path, nil)

        context.textMatrix = CGAffineTransform.identity
        CTFrameDraw(frame, context)

    }

    /// 文字認識情報の配列取得 (非同期)
    private func getTextObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNRecognizedTextObservation])->())) {
        let request = VNRecognizeTextRequest { (request, error) in
            guard let results = request.results as? [VNRecognizedTextObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        request.recognitionLevel = recognitionLevel
        request.recognitionLanguages = supportedRecognitionLanguages
        request.usesLanguageCorrection = true

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }

    /// 文字検出位置に矩形を描画した image を取得
    private func getTextRectsImage(sampleBuffer :CMSampleBuffer, textObservations: [VNRecognizedTextObservation]) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        let bitmapInfo = CGBitmapInfo(rawValue:
            (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        )

        guard let newContext = CGContext(
            data: pixelBufferBaseAddres,
            width: width,
            height: height,
            bitsPerComponent: 8,
            bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: bitmapInfo.rawValue
            ) else
        {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let imageSize = CGSize(width: width, height: height)

        textObservations.forEach{
            let rect = getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
            let text = $0.topCandidates(1).first?.string ?? "" // topCandidates に文字列候補配列が含まれている
            self.drawRect(rect, text: text, context: newContext)
        }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let imageRef = newContext.makeImage() else {
            return nil
        }
        let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.right)

        return image
    }
}


extension TextObservationViewController : AVCaptureVideoDataOutputSampleBufferDelegate{

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getTextObservations(pixelBuffer: pixelBuffer) { [weak self] textObservations in
            guard let self = self else { return }
            let image = self.getTextRectsImage(sampleBuffer: sampleBuffer, textObservations: textObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }
}

github

becky3/text_observation: Vision.frameworkでカメラ画像のテキスト認識を行う
https://github.com/becky3/text_observation

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterでiOSアプリ開発入門(1/-)

FlutterでiOSアプリ開発入門

本記事はmacOSユーザーを想定しています。
ほとんどGoogleが提供するドキュメントの直訳です?

なぜFlutterなのか

  1. 技術的資産性が高い
  2. メンテナンスされている
  3. 学習コストが低い

Googleによるメンテナンス、発展の余地、クロスプラットフォーム、ホットリロードなど様々な要素から技術的な資産性が高いな〜と感じています
またβ版のFlutter for Webも気になります…!
image.png

【第一部】導入編

こちらのドキュメントに従ってインストールしていきます
https://flutter.dev/docs/get-started/install/macos

Get started

ステップ1と2はgitからcloneすることで省略可能

$ git clone https://github.com/flutter/flutter.git

1. Download the Flutter SDK

Get the Flutter SDKからSDKをダウンロードします
https://flutter.dev/docs/get-started/install/macos

2. Locate the file

解凍したファイルを任意のディレクトリに置きます(ドキュメントではDevelopmentというディレクトリに設置)

$ mkdir ~/Development
$ cd ~/Development
$ unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip

3. Add the flutter tool to your path

.zshrcに以下を記載

export PATH="$PATH:`pwd`/Development/flutter/bin"

sourceで反映

source .zshrc

4. (Optional) Pre-download development binaries

一応ダウンロードしておきます。

$ flutter precache

5. Run flutter doctor

Flutterがインストールされていることを確認します。

$ flutter doctor

Flutterのインストールが確認できました。

Tips
ルートディレクトリ以外でflutterコマンドが認識されなかったりする場合はターミナルを再起動してみよう

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✅] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.14.6 18G1012, locale ja-JP)
[❌] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/setup/#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, set ANDROID_HOME to that location.
      You may also want to add it to your PATH environment variable.

[❗️] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
[❗️] Android Studio (not installed)
[❗️] VS Code (version 1.40.1)
    ✗ Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[❗️] Connected device
    ! No devices available

! Doctor found issues in 5 categories.

iOS setup

1. Install Xcode

まだXcodeがインストールされていない場合は、Mac App Storeから事前にインストールしてください。

$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch

2. Set up the simulator

iOSデバイスのシュミレーターを立ち上げます

$ open -a Simulator

3. Create a simple Flutter app

いよいよアプリを作成!

$ flutter create sample_app
$ cd sample_app
$ flutter run

?シミュレーターを起動していなかった場合

$ flutter run
No supported devices connected.

事前に立ち上げておく

$ open -a Simulator

?iPhoneを接続していた場合

実機で動かすにはセットアップが必要なので一旦はシミュレーターに頼ります

$ flutter run
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

k3ntar0-iPhoneXs  • 00008020-001E1DAA26------            • ios • iOS 13.3
iPhone 11 Pro Max • 3B03E8AB-04A8-4CED-A35F-523900------ • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-3 (simulator)
$ flutter run -d <deviceId>

無事立ち上がりました!

4. Deploy to iOS devices

先ほどはシミュレーターで動かしましたが、次は実機で動かせるようにします

a. Install CocoaPods

$ sudo gem install cocoapods
$ pod setup

b. Open Runner

RunnerのプロジェクトがXcodeで開きます

open ios/Runner.xcworkspace

c. Setup Xcode

XcodeはUIがめちゃくちゃわかりにくいので気を強く持ってください

  1. Runnerを選択
  2. Signing & Capabilitiesを選択
  3. Teamを設定
  4. Bundle Identifierを設定

このあたりは他の記事に説明を譲ります

d. Launch app in real iPhone

sample_app が追加されています!

IMG_4176B29C3C6A-1.jpeg

第一部

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterでiOSアプリ開発入門(第一部)

FlutterでiOSアプリ開発入門

本記事はmacOSユーザーを想定しています。
ほとんどGoogleが提供するドキュメントの直訳です?

なぜFlutterなのか

  1. 技術的資産性が高い
  2. メンテナンスされている
  3. 学習コストが低い

Googleによるメンテナンス、発展の余地、クロスプラットフォーム、ホットリロードなど様々な要素から技術的な資産性が高いな〜と感じています
またβ版のFlutter for Webも気になります…!
image.png

【第一部】 導入編

こちらのドキュメントに従ってインストールしていきます
https://flutter.dev/docs/get-started/install/macos

Get started

ステップ1と2はgitからcloneすることで省略可能

$ git clone https://github.com/flutter/flutter.git

1. Download the Flutter SDK

Get the Flutter SDKからSDKをダウンロードします
https://flutter.dev/docs/get-started/install/macos

2. Locate the file

解凍したファイルを任意のディレクトリに置きます(ドキュメントではDevelopmentというディレクトリに設置)

$ mkdir ~/Development
$ cd ~/Development
$ unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip

3. Add the flutter tool to your path

.zshrcに以下を記載

export PATH="$PATH:`pwd`/Development/flutter/bin"

sourceで反映

source .zshrc

4. (Optional) Pre-download development binaries

一応ダウンロードしておきます。

$ flutter precache

5. Run flutter doctor

Flutterがインストールされていることを確認します。

$ flutter doctor

Flutterのインストールが確認できました。

?Tips ルートディレクトリ以外でflutterコマンドが認識されなかったりする場合はターミナルを再起動してみよう

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✅] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.14.6 18G1012, locale ja-JP)
[❌] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from: https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK components.
      (or visit https://flutter.dev/setup/#android-setup for detailed instructions).
      If the Android SDK has been installed to a custom location, set ANDROID_HOME to that location.
      You may also want to add it to your PATH environment variable.

[❗️] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
        sudo xcodebuild -runFirstLaunch
[❗️] Android Studio (not installed)
[❗️] VS Code (version 1.40.1)
    ✗ Flutter extension not installed; install from
      https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter
[❗️] Connected device
    ! No devices available

! Doctor found issues in 5 categories.

iOS setup

1. Install Xcode

まだXcodeがインストールされていない場合は、Mac App Storeから事前にインストールしてください。

$ sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
$ sudo xcodebuild -runFirstLaunch

2. Set up the simulator

iOSデバイスのシュミレーターを立ち上げます

$ open -a Simulator

3. Create a simple Flutter app

いよいよアプリを作成!

$ flutter create sample_app
$ cd sample_app
$ flutter run

?Error シミュレーターを起動していなかった場合

$ flutter run
No supported devices connected.

事前に立ち上げておく

$ open -a Simulator

?Error iPhoneを接続していた場合
実機で動かすにはセットアップが必要なので一旦はシミュレーターに頼ります

$ flutter run
More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

k3ntar0-iPhoneXs  • 00008020-001E1DAA26------            • ios • iOS 13.3
iPhone 11 Pro Max • 3B03E8AB-04A8-4CED-A35F-523900------ • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-3 (simulator)
$ flutter run -d <deviceId>

無事立ち上がりました!

4. Deploy to iOS devices

先ほどはシミュレーターで動かしましたが、次は実機で動かせるようにします

a. Install CocoaPods

$ sudo gem install cocoapods
$ pod setup

b. Open Runner

RunnerのプロジェクトがXcodeで開きます

open ios/Runner.xcworkspace

c. Setup Xcode

XcodeはUIがめちゃくちゃわかりにくいので気を強く持ってください

  1. Runnerを選択
  2. Signing & Capabilitiesを選択
  3. Teamを設定
  4. Bundle Identifierを設定

このあたりは他の記事に説明を譲ります

d. Launch app in real iPhone

sample_app が追加されています!

IMG_4176B29C3C6A-1.jpeg

第二部に続く?

(coming soon)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TextFieldタップでDataPickerを呼ぶ

下図のように編集開始時にDatePickerを起動させ,時間変更時に変更時間をTextFieldに反映させます.

ViewController.swift
var toolBar:UIToolbar!
@IBOutlet var wakeTimeTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    wakeTimeTextField.delegate = self
    setupToolbar()
}
func setupToolbar() {
        //datepicker上のtoolbarのdoneボタン
        toolBar = UIToolbar()
        toolBar.sizeToFit()
        let toolBarBtn = UIBarButtonItem(title: "DONE", style: .plain, target: self, action: #selector(doneBtn))
        toolBar.items = [toolBarBtn]
        wakeTimeTextField.inputAccessoryView = toolBar
}

func textFieldDidBeginEditing(_ textField: UITextField) {
    let datePickerView:UIDatePicker = UIDatePicker()
    datePickerView.datePickerMode = UIDatePicker.Mode.time
    textField.inputView = datePickerView
    datePickerView.addTarget(self, action: #selector(datePickerValueChanged(sender:)), for: UIControl.Event.valueChanged)
}

    //datepickerが選択されたらtextfieldに表示
@objc func datePickerValueChanged(sender:UIDatePicker) {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat  = "H:mm";
    wakeTimeTextField.text = dateFormatter.string(from: sender.date)
}

//toolbarのdoneボタン
@objc func doneBtn(){
    wakeTimeTextField.resignFirstResponder()
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TextFieldタップでDatePickerを呼ぶ

下図のように編集開始時にDatePickerを起動させ,時間変更時に変更時間をTextFieldに反映させます.

まずTextFieldのDelegateを追加します.

ViewController.swift
class ViewController: UIViewController, UITextFieldDelegate 

次に以下のコードを追加hします

ViewController.swift
var toolBar:UIToolbar!
@IBOutlet var wakeTimeTextField: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()

    wakeTimeTextField.delegate = self
    setupToolbar()
}
func setupToolbar() {
        //datepicker上のtoolbarのdoneボタン
        toolBar = UIToolbar()
        toolBar.sizeToFit()
        let toolBarBtn = UIBarButtonItem(title: "DONE", style: .plain, target: self, action: #selector(doneBtn))
        toolBar.items = [toolBarBtn]
        wakeTimeTextField.inputAccessoryView = toolBar
}

func textFieldDidBeginEditing(_ textField: UITextField) {
    let datePickerView:UIDatePicker = UIDatePicker()
    datePickerView.datePickerMode = UIDatePicker.Mode.time
    textField.inputView = datePickerView
    datePickerView.addTarget(self, action: #selector(datePickerValueChanged(sender:)), for: UIControl.Event.valueChanged)
}

    //datepickerが選択されたらtextfieldに表示
@objc func datePickerValueChanged(sender:UIDatePicker) {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat  = "H:mm";
    wakeTimeTextField.text = dateFormatter.string(from: sender.date)
}

//toolbarのdoneボタン
@objc func doneBtn(){
    wakeTimeTextField.resignFirstResponder()
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift 5.1 Tour まとめ

概要

Swift開発経験0.001程度のプログラマーが少しでもSwift理解を深めるためにA Swift Tourの内容をまとめました。

ただ、そもそもツアーだけでは理解をすることができない点が多々あったので、自分で追記したり、参考記事のリンクを添付しています。
そのため完全なSwiftTourのただのまとめではなく自分なりにアレンジを加えたものになります。
まとめといったな?あれは嘘だ

※Tourで使われているコードはそのまま添付しています。検証の際はMyPlaygroundのご利用をおすすめします。

本文

Simple Values

変数と定数

変数はvarで、定数はletで定義する。

// 変数を定義
var myVariable = 42
myVariable = 50 // 上書き

// 定数を定義
let myConstant = 42
myConstant = 11 // エラーになる

以下の形式で、変数や定数に型を明示することができる。

let explicitDouble: Double = 70 // コロンの後のDoubleが型の定義。

※型の一覧と意味はこちらを参照。

下記のように変数の結合で、型がマッチしていない場合、エラーになるので注意。

let label = "The width is " // 文字列はダブルクオーテーションで囲む
let width = 94
let widthLabel = label + String(width) // widthがInt型のため、string型に変換

文字列内で変数を展開する場合は\()で変数を囲う。

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

配列と辞書

配列型と辞書型の値は[]で囲う。 (配列と辞書について)

// 配列
var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"
// 配列に追加
shoppingList.append("blue paint")

// 辞書
var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
// 辞書の追加
occupations["Jayne"] = "Public Relations"

※辞書型はPHPでいう連想配列といったところか。
※辞書型についてはこちらも参照。

空の配列型、辞書型の定義もできる。

// 空配列
let emptyArray = [String]() // 値の型を定義
// 空辞書
let emptyDictionary = [String: Float]() // キーと値の型をそれぞれ定義

すでに定義されている配列・辞書の初期化は以下のように書く。

// 配列の初期化
shoppingList = []
// 辞書の初期化
occupations = [:]

Control Flow

条件はifswitchで、ループはfor-inwhilerepeat-whileで書く。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
// inの後にループさせる変数を置く
// forの後のteamScoreにループの結果取得できる値が格納される
for score in individualScores { 
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)
// Prints "11"

詳しくはこちらも参照。

オプショナル型とif let

オプショナル型は、変数の型のひとつで、nilの可能性がある値を指す。
nilが考えられるような変数をチェックする場合、if letを使うと良い。

var optionalName: String? = "John Appleseed" // オプショナル型変数
var greeting = "Hello!"
// optionalNameがnilのとき、条件節内は通らない。(falseのため)
if let name = optionalName {
    greeting = "Hello, \(name)"
}

optionalNameの定義で使われているStringの後の?で、オプショナル型を定義。

下記のように??を使って文字列内でnilかどうかの判定をすることもできる。

let nickName: String? = nil
let fullName: String = "John Appleseed"
// nickNameがnilなら、fullNameが使用される
let informalGreeting = "Hi \(nickName ?? fullName)"

switch構文

switch caseを使えば、いろいろなかたちでの比較ができる。

let vegetable = "red pepper"
switch vegetable {

    // 文字列の比較。一致していたら「Add some raisins and make ants on a log.」が表示される。
    case "celery":
        print("Add some raisins and make ants on a log.")

    // 同じ結果を出力する場合、比較対象をカンマで区切って書くこともできる。
    case "cucumber", "watercress":
        print("That would make a good tea sandwich.")

    // 変数xが「pepper」という文字を含む場合、letの後の定数xに「pepper」を格納する
    case let x where x.hasSuffix("pepper"):
        print("Is it a spicy \(x)?")

    // defaultを設定しないとエラーになる
    default:
        print("Everything tastes good in soup.")
}
// Prints "Is it a spicy red pepper?"
// 上記いずれかのcaseに合致すれば、switch構文は終了する。
// ※caseに一致しない場合、defaultが出力される。
// (続くcaseが比較されることはない)

for-in構文

for inを使えば、ループ対象の配列・辞書のキーと値のそれぞれを取り出せる。

// 辞書型
let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0
// kindで「Prime」などのキーを、numbersで配列を取り出せる
for (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)
// Prints "25"

while構文

while構文は、while後に書かれた条件に該当する限り構文内を通る。

var n = 2
while n < 100 {
    n *= 2
}
print(n)
// Prints "128"

for 変数 in 開始値 ..< 終了値で、+1ずつ変数に格納され、変数が終了値と同値の場合、条件節内を通らない。

var total = 0
for i in 0..<4 {
    total += i
}
print(total)
// Prints "6"

for 変数 in 開始値 ... 終了値は、終了値と同値でも条件節内を通り、超過した場合falseとなり、条件節内を通らない。

Functions and Closures

functionはfuncで始めて定義する。

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
// functionの実装
greet(person: "Bob", day: "Tuesday")

persondayが引数にあたるラベルであり、それぞれのラベルのコロンの後のStringが値の型を表す。
->後のStringは、返り値(戻り値)の型を表す。
また、_のみ記述してラベルを省略することも可能。

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
// 「_」を用いることで、第一引数はラベルが不要。
greet("John", on: "Wednesday")

タプル

返り値はタプルを使って複数指定することも可能。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }

    return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
// Prints "120"
print(statistics.2)
// Prints "120"

ネスト

関数をネスト(入れ子)構造で書くこともできる。

func returnFifteen() -> Int {
    var y = 10
    func add() {
        // 上位の関数で定義された変数にアクセスすることもできる
        y += 5
    }
    add()
    return y
}
print(returnFifteen())
// 15

ファーストクラスオブジェクト

Swiftでは関数は「ファーストクラスオブジェクト」として定義されている。
よって、値として関数を用いることもできる。

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

また、引数に関数を入れることもできる。

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
// 第二引数でlessThanTenという関数を用いている
hasAnyMatches(list: numbers, condition: lessThanTen)

クロージャ

クロージャは、たとえば次のように書く。

var numbers = [20, 19, 7, 12]
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
// Prints "[60, 57, 21, 36]"

他にも、inなどを省略することができる。

var numbers = [20, 19, 7, 12]
let sortedNumbers = numbers.sorted { $0 > $1 } // $0と$1については下記のクロージャの記法について参照を
print(sortedNumbers)
// Prints "[20, 19, 12, 7]"

※クロージャの記法についてはこちらを参照

Objects and Classes

クラスの定義はclassで始める。

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

定義したクラスは以下のように用いる。

var shape = Shape()
// ShapeクラスのnumberOfSides変数に 7 を格納
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
print(shapeDescription)
// A shape with 7 sides.

イニシャライザ

インスタンスの生成時、イニシャライザinitでの初期化をする必要がある。
※その理由についてはこちら

class NamedShape {
    var numberOfSides: Int = 0
    var name: String
    // name初期化 イニシャライザのため、funcは不要
    init(name: String) {
        // selfで自クラス内の変数を指示する。(initのnameを指していない)
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

オブジェクトのクリーンアップ処理をする場合、deinitデイニシャライザを生成する。

継承

継承の概念があり、親クラスの属性を引き継ぐためには、子クラスの後に: 親クラスを記載する。

// SquareがNamedShapeクラスを継承
class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

親クラスにある関数と同一の関数名を用いる場合、overrideをつけて定義。(なければエラーになる。)

ゲッター・セッター

ゲッターとセッターを利用することもできる。

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        // 値の取得
        get {
            return 3.0 * sideLength
        }
        // 値のセット
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triangle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
// Prints "9.3" ゲッター
triangle.perimeter = 9.9
print(triangle.sideLength)
// Prints "3.3000000000000003" セッター

上記のEquilateralTriangleクラスでは、3つのステップが踏まれている。

  • サブクラスが宣言するプロパティの値をセット
  • サブクラスのイニシャライザの呼び出し
  • スーパークラス(親クラス)で定義されたプロパティの値を変更。

willSet と didSet

willSet didSetでプロパティの変更前、変更後で何らかの処理を行わせることができます。

class TriangleAndSquare {
    // 先のEquilateralTriangleクラスを定義
    var triangle: EquilateralTriangle {
        willSet {
            // newValueはwillSetの定数
            square.sideLength = newValue.sideLength
        }
    }
    // 先のSquareクラスを定義
    var square: Square {
        didSet {
            // oldValueはdidSetの定数
            triangle.sideLength = oldValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
// Prints "10.0"
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"

結果がオプショナル型の場合があるとき、Square?のように?をつける。
オプショナル型の変数ないし定数を利用する際は、optionalSquare?のように対象のプロパティの後に?をつける。

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

Enumerations and Structures

enum

列挙型を書く際は、enumで始める。

enum Rank: Int {
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king
    // 列挙型でも関数を書くこともできる
    func simpleDescription() -> String {
        switch self {
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}

let ace = Rank.ace
print(ace)
// ace

let aceRawValue = ace.rawValue // rawValueで要素の値を取得
print(aceRawValue)
// 1

列の要素それぞれの値は、先頭から012...というように順番に割り当てられる。
(例:case a, b, cという列なら、aには0が、bには1cには2が割り当てられる。)

ただ、case ace = 1のように要素に対して明示的に値を割り当てることによって、要素の値を変更することができる。
(例:case ace = 1では明示的に1を要素の値に割り当てていますが、そうでなければ0が入る。)

また、続く要素の値は2、3...というように明示された値に続く。

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

上記のようにインスタンス生成時にrawValueを指定して列挙子を取得する際、自動的にイニシャライザーinit(rawValue:)が付与される。

enum Suit {
    case spades, hearts, diamonds, clubs

    func simpleDescription() -> String {
        switch self {
        case .spades:
            return "spades"
        case .hearts:
            return "hearts"
        case .diamonds:
            return "diamonds"
        case .clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()

自クラス外の特定の要素にアクセスする場合は、Suit.heartsのように要素の前にSuitをつける必要があるが、クラス内では.heartsのようにクラスを省略できる。
switch文のselfで、引数自体にアクセスできる。

下記のように要素を定数に格納し、要素と一致する値を抽出することもできる。

enum ServerResponse {
    case result(String, String)
    case failure(String)
}

let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")

switch success {
case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
    print("Failure...  \(message)")
}
// Prints "Sunrise is at 6:00 am and sunset is at 8:09 pm."

構造体

structをつけて構造体を生成できる。イニシャライザを持つことができるが、継承することはできず、クラスが参照型であるのに対し、構造体は値型。

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

Protocols and Extensions

プロトコルは以下のように定義する。

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

クラスも列挙も構造体もプロトコルを継承することができる。

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

mutatingstructenumにおいて自身の値を変更する場合にfuncの前につける。(変更できることを明示する必要がある)
classではつける必要がない。クラスは常に変更できるから。

extension

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)
// Prints "The number 7"

extensionをつけると既存の機能を拡張できる。
上記の例でいうと、ExampleProtocolの型プロパティをIntで拡張し、adjustメソッドをExampleProtocolに追加している。

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// Prints "A very simple class.  Now 100% adjusted."
// print(protocolValue.anotherProperty)  // Uncomment to see the error

プロトコル名は型としても利用できる。
また上記の例では、protocolValue変数はすでにSimpleClassクラスで利用されているが、コンパイラがExampleProtocolの変数として認識するため問題ない。クラスが実装するプロパティにアクセスできない。

Error Handling

エラーを取得する際はErrorプロトコルを用いる。

enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

エラーハンドリングはthrowを用い、エラーハンドリングをする関数を用いる場合はthrowsを用いる。

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

下記の例ではIf節を通った場合、即座にエラーを返す。

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

エラーハンドリングの方法は他にもある。

do-catch

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}
// Prints "Job sent"

do-catchでは、doブロック内でtry文を書くことで、エラー時にcatchのエラーコードが自動的に出力される。

下記のように複数のcatchブロックを用意することも可能。

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    print(printerResponse)
} catch PrinterError.onFire {
    print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
    print("Printer error: \(printerError).")
} catch {
    print(error)
}
// Prints "Job sent"

try?

try?と書けばオプショナル型で結果を返せる。関数がエラーを返せば、結果ではnilが返る。

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

defer

deferを使ってブロック要素を書くことで最終的には関数内のコードをすべて通す。(returnするまで)

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }

    let result = fridgeContent.contains(food)
    return result
}
fridgeContains("banana")
print(fridgeIsOpen)
// Prints "false"

Generics

ジェネリクスで、IntやStringなどの予め指定された型とは異なり、任意の指定した型パラメータを用いることができる。
<>(山括弧)で括ることで、ジェネリクスの関数・型を定義することができる。
※下記でいうと<Item>がジェネリクス。

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

ジェネリクスは、クラス、列挙、構造体でも用いることができる。

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

where

下記のようにwhereを関数内部の前に記述することで、2つの型が同じであること、またはあるクラスがスーパークラスを継承していることを明示することができる。

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
    where T.Element: Equatable, T.Element == U.Element
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
anyCommonElements([1, 2, 3], [3])

<T: Equatable>と、<T> ... where T: Equatableは同じ意味。

End

最後に

先人の知恵を勝手にたくさん拝借しました。ありがとうございます。
(リンク先の記事がどれもわかりやすく、理解に際して非常にありがたかったです…)

修正や参考資料などございましたらご指定いただけますと幸いです。

ご覧くださいましてありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2019年12月版】mitmproxyでアプリの通信内容を確認したい

アプリ開発初心者なので

先日、自社で作成したアプリの最終チェックに関わったんですが、その時に「通信内容見たいな~」って思ったんですよ。まあサーバ側が見れればそこで見ればいいんですけど、昨今のアプリはいろんなところと通信するので、すべての通信内容を見れなかったりする事情もあるわけで。なので、そういう時に気軽に通信内容をチェックできるMan-In-The-Middleなproxyを立ててみようと思いました。

mitmproxy

ググると一発目でこいつに行き当たりました。ふーん、Fiddlerじゃないんだ。
Qiitaにも当然のように記事がたくさんあったので、まずは入れてみて動かしてみましょう。

導入手順

WSL導入

動作環境はWindows 10なので、まずはWSLを有効化します。
スタートメニューを右クリック->アプリと機能を選択。
2019-12-21_11h13_18.png

アプリと機能を選んでいきます。
2019-12-21_11h15_16.png

次にWindowsの機能の有効化・・・を選びます。
2019-12-21_11h15_41.png

ここでWSLを有効化します。
2019-12-21_11h17_25.png

次にWindowsの設定を開き、
2019-12-21_12h18_28.png

開発者モードをOn!
2019-12-21_12h18_54.png

再起動が求められたりするので、立ち上がったらWindows Storeでubuntuを探しましょう。
2019-12-21_12h22_13.png

はい。特に理由がなければLTSで。
2019-12-21_12h22_29.png

無事立ち上がります。
2019-12-21_12h26_02.png

mitmproxyのインストール

mitmproxyの導入はaptのおかげで至極簡単です。

sudo apt install python3-pip && sudo pip3 install -U pip && sudo pip3 install mitmproxy

2019-12-21_12h28_56.png

mitmproxyの起動と端末側の準備

シェルでmitmproxyとたたけば立ち上がります。
2019-12-22_07h44_17.png

SHIFT + Fでfollowモード(tailみたいな感じ)にしておきましょう。

これでmitmproxyが起動しましたが、準備はまだ終わっていません。端末側にproxyの設定と、mitmproxyの証明書をインストールする必要があります。
まずはproxyの設定。ポート指定してない場合は8080です。
1.png

次に証明書です。
端末側でブラウザを開き、mitm.itとアドレスに打ち込みます。
2.png

利用する端末のアイコンを選択して、証明書をダウンロードします。
IMG_0104.PNG

iOSの場合、これだけでは証明書が有効にならないようです。
設定を開くとプロファイルのインストールがアラートとして出てくるので、それを選択します。

IMG_0108.PNG

IMG_0105.PNG

最後に、設定 > 一般 > 情報 > 証明書信頼設定で有効にしておく必要があります。
IMG_0111.PNG

使ってみる。

まずはqiitaでも見てみましょう。(ブラウザですが)
IMG_0119.PNG

mitmproxyの方。よしよし。丸見えですね。
2019-12-22_08h13_52.png

アプリも見てましょう。例としてsmartnewsを開いた時の通信を見てみました。
2019-12-22_08h16_59.png

いい感じです。

見れないアプリもある。

証明書のpinning(これ、なんて訳すんでしょうね)という方法で中間者攻撃を無効化するというのは、比較的いろいろなアプリで実施されているようです。
先ほどsmartnewsの通信を垣間見ましたが、yahoo newsでは通信を見ることができません。証明書のpinningに対応しているものと思われます。
3.png

同様に、以前は通信内容を見ることができたAmazonなどのアプリも、中間者攻撃に対して対策がされているようで、mitmproxy動作下では正常に動作しないようです。
対策がされているアプリとそうでないアプリを調べていくのもなかなか楽しそうです。
逆に言うと、自分たちが作るアプリでも同様のことをしなくてはいけないケースを想定しなくてはいけませんね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった

:santa:この記事は、 North Detail Advent Calendar 2019 の22日目の記事です:christmas_tree:

概要

やりたいこと

  1. ピアノ演奏中の手のモーションをスマホカメラで記録する
  2. Unityでアニメーションしてピアノ演奏する

技術的なこと

  1. google/mediapipeでハンドトラッキング用のiOSアプリをビルド
  2. iPhoneカメラで手のモーションを記録
  3. Blenderでアニメーション化 & ピアノオブジェクト作成
  4. Unityに取り込んで再生、指と鍵盤の当たり判定で音を鳴らす

成果物

※雑音が流れるのでご注意を

......はい(出落ち)

作業環境

・MacBook Pro (Retina, 15-inch, Mid 2014) : Mojave 10.14.6
・iPhoneXs : iOS 13.2.2

・Xcode : 11.3
・Blender : 2.79
・Unity : 2019.2.12

今回はiPhoneのカメラを利用しますが、AndroidやPCのカメラでも同じことができるはず
後述しますが、PCカメラ利用の場合はLinuxでGPUが利用できる環境推奨

Google / MediaPipeでモーション記録用のアプリをビルド

MediaPipeってなぁに?

MediaPipe is a framework for building multimodal (eg. video, audio, any time series data) applied ML pipelines. With MediaPipe, a perception pipeline can be built as a graph of modular components, including, for instance, inference models (e.g., TensorFlow, TFLite) and media processing functions.

MediaPipeは、マルチモーダル(ビデオ、オーディオ、時系列データなど)を適用したMLパイプラインを構築するためのフレームワークです。 MediaPipeを使用すると、たとえばパイプラインモデル(TensorFlow、TFLiteなど)やメディア処理機能など​​、モジュラーコンポーネントのグラフとして認識パイプラインを構築できます。

......なるほど:thinking:(?)

どうやら機械学習パイプライン(画像処理→モデル推論→描画 など)の構築や、
それをグラフとして視覚化できるフレームワークらしい

サンプルではTensorFlowやTensorFlow Liteなどを利用して、
顔認識や物体の識別、手のモーション取得(ハンドトラッキング)などができる

Linux/Win/Macで実行したり、iOS/Android用にアプリをビルドしたりできる

MediaPipeをインストール

0. PCで実行したい人向け

モバイルアプリのビルドではなく、PCで直接実行したい人はLinux環境(OS Xは含まない)推奨です

2019/12/11現在、MediaPipeではデスクトップ用サンプルはLinuxのみGPUサポートが対応しています
(Win/MacでもCPU実行モードはあるのですが私の環境では即座にフリーズしました......)

また、VMやdocker上のLinux環境ではホストGPUが使えないため、
MacならデュアルブートでOSインストールなどが必要です
(これを知らずにVirtualBoxのUbuntu上で作業していて土日まるまる潰しました......超頑張ればできないこともないとかなんとか?)

1. 事前準備

インストールガイドページInstalling on macOSを参考に進めていきます

  • Homebrewをインストール
  • XcodeCommand Line Toolsをインストール

  • Pythonバージョンの確認
    私の環境ではPython 2.7ではビルドが通らなかったため、下記サイトを参考にPython 3.7.5をインストールしました
    参考:pyenvを使ってMacにPythonの環境を構築する

  • "six"ライブラリをインストール

$ pip install --user future six
2. MediaPipeリポジトリをクローン
$ git clone https://github.com/google/mediapipe.git
$ cd mediapipe
3. Bazelをインストール

二通りの方法がありますが、今回はOption 1の方法でやります
※2019/12/11現在、MacではBazel 1.1.0より上のバージョンは対応していないため注意

# Bazel 1.1.0より上のバージョンがインストールされている場合はアンインストール
$ brew uninstall bazel

# Install Bazel 1.1.0
$ brew install https://raw.githubusercontent.com/bazelbuild/homebrew-tap/f8a0fa981bcb1784a0d0823e14867b844e94fb3d/Formula/bazel.rb
$ brew link bazel
4. OpenCVとFFmpegをインストール

こちらも二通りあるのでOption 1の方法でインストール

$ brew install opencv@3
5. Hello World desktop exampleを実行
$ export GLOG_logtostderr=1
$ bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
    mediapipe/examples/desktop/hello_world:hello_world

# しばし待ったのち以下が表示されたらOK
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!

ここで私の環境ではPython2.7で以下のエラーが発生しました
同じ現象が起こった人は1. 事前準備を参考にPythonのバージョンを上げてみてください

ERROR: An error occurred during the fetch of repository 'local_config_git':
   Traceback (most recent call last):

モバイルアプリのビルド

今回は両手のモーションを取得するモバイルアプリをビルドしたいので、以下のサンプルを利用します
Multi-Hand Tracking (GPU)

※PCで直接実行したい場合は以下を利用してください
Multi-Hand Tracking on Desktop

Androidの場合

Androidの方が簡単にビルドできます
トラッキングには2Dと3Dモードの二通りあるので、3Dモードでビルドします

$ bazel build -c opt --config=android_arm64 --define 3D=true mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu

かなり時間がかかるので待ちましょう
下記ディレクトリにapkファイルができるので実機にインストールすれば完了です!

mediapipe/bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu/multihandtrackinggpu.apk

iOSの場合

iOSの場合はこちらの設定がもうひと手間必要です
始めの方にあるXcodeやBazelのインストールは既に完了しているので不要です

(しかもこちらの方法でBazelをインストールすると最新バージョンを取得して動かなくなるという罠。間違えてインストールしてしまった場合は前述の方法でv1.1.0に置き換えてください)

1. Provisioning Profileの用意

AppleDeveloperProgram加入者はデベロッパサイトでProvisioning Profileを作成してダウンロードしてください

そうでない方は以下のサイトなどを参考に作成してください
参考:[Xcode][iOS] 有料ライセンスなしでの実機インストール 全工程解説!

Bundle Identifierを固有のものにしないとエラーが起きるので注意
どこかの誰かが使っているIDだとFailed to create provisioning profile.と怒られます
作成されたファイルは以下のpathにあります

~/Library/MobileDevice/Provisioning Profiles/

用意したファイルはprovisioning_profile.mobileprovisionにリネームして、
mediapipe/mediapipe/に配置します

2. BUILDファイルの修正

mediapipe/mediapipe/examples/ios/multihandtrackinggpu/BUILD45行目のbundle_idを、
Provisioning Profileで設定したBundle Identifierに変更

mediapipe/examples/ios/multihandtrackinggpu/BUILD:45
bundle_id = "com.google.mediapipe.MultiHandTrackingGpu",
         ↓
bundle_id = "hoge.fuga.piyo",
3. アプリのビルド

3Dモードでビルドします

$ bazel build -c opt --config=ios_arm64 --define 3D=true mediapipe/examples/ios/multihandtrackinggpu:MultiHandTrackingGpuApp

長いのでしばらく待ちます
ビルドが完了すると下記ディレクトリにipaファイルができます

bazel-bin/mediapipe/examples/ios/multihandtrackinggpu/
4. 実機にインストール

XcodeのWindow > Devices and Simulatorsから、
USB接続した実機デバイスを選択して、
画面下部の+ボタンからipaファイルをインストールすれば完了です!

手の動きをトラッキング

ビルドしたアプリを動かしてみる

hand_tracking.gif

おお!両手の動きがリアルタイムで反映されていますね

このときのiPhoneのプロセス状態を、USB接続したMacのコンソールアプリで確認してみましょう
スクリーンショット 2019-12-12 23.31.10.png
MultiHandTrackingGpuAppというプロセス名で何やらログが表示されています
hand[0]hand[1]の二つあり、それぞれ21個のLandmarkを持っています

先程のgifの片手のポイントの数が21なので、その座標のようですね
それぞれの手のログは約0.05秒毎に表示されているようです

ちなみに、ログ出力のコードは下記ファイルに記述されていました
mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm : L177あたり

フロントカメラではなく背面カメラを使用したい場合は、同ファイルの以下を変更してビルドし直すと可能です

mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm
// 100行目をYESからNOに
_renderer.mirrored = NO;

// 109行目を~Frontから~Backに
_cameraSource.cameraPosition = AVCaptureDevicePositionBack;

Unityで表示してみる

取得した座標をUnityの3D空間に浮かべてみます
スクリーンショット 2019-12-12 21.45.01.png
なぁにこれぇ?

ログをよくみるとz座標だけ数値が異様に大きいですね
倍率を変更してみます
スクリーンショット 2019-12-12 22.03.27.png
おっ!なんかそれっぽくなった!
向きと大きさを調整して線で繋いでみると・・・
スクリーンショット 2019-12-12 23.28.34.png
完全に手だコレ! \\ ٩( 'ω' )و//

あとはピアノを演奏しているところを撮影して座標を取得するだけです

Blenderでアニメーション化

Blenderとは?
  • 3Dモデル作成、アニメーション作成、レンダリングなどができる
  • 高機能・高品質、だけど高難度
  • オープンソースで無料で使える

最近 v2.8がリリースされ、UIや操作方法が変わり直感的に操作できるようになりました
参考サイトはv2.7xの方が多く、古いプロジェクトをインポートすると壊れてしまう場合もあるため今回はv2.79を使用します

「Blender アニメーション 作成」で検索すると、
手動でボーンを動かしてキーフレーム毎にポーズを設定する方法が主流のようです
参考:かんたんBlender講座 アニメーションの基本

この方法は単純なアニメーションだとよいのですが、
ミリ秒単位で複雑な動きをさせる場合は骨が折れます(ボーンだけに)

そのため今回はスクリプトで作成します

Pythonでスクリプト作成

BlenderではPythonスクリプトが実行できます
MediaPipeで取得した座標をCSVファイルにまとめて、スクリプトからアニメーション化します

1. 座標CSVファイルの作成

こんな感じでCSVファイルを作成しました
左から以下の値となってます

  • 手(0=左手、1=右手)
  • Landmark
  • 開始からの秒数
  • x座標
  • y座標
  • z座標

スクリーンショット 2019-12-22 3.21.21.png

2. Landmarkオブジェクトの作成

以下のSphereオブジェクトを追加
これをLandmarkポイントとして動かします

  • Left.000 ~ Left.020
  • Right.000 ~ Right.020
3. スクリプトの作成

Pythonスクリプトはこんな感じ

投稿時間を過ぎてしまったので説明は省略...:bow_tone1:
(倍率とかfpsとか適当。。。)

コメント追記しました

import bpy
import os

leftLandmarks = []
rightLandmarks = []
fps = 24

scale = (0.015,0.015,0.015)

# Landmarkオブジェクトを割当
for i in range(21):
    leftLandmarks.append(bpy.data.objects["Left.0" + str(i).zfill(2)])
    leftLandmarks[i].scale = scale

for i in range(21):
    rightLandmarks.append(bpy.data.objects["Right.0" + str(i).zfill(2)])
    rightLandmarks[i].scale = scale

# csvのあるディレクトリ
dirpath = "/Path/to/csv"
filename = "animation.csv"

filepath = os.path.join(dirpath, filename)

# csvを一行ずつ処理
with open(filepath, mode='rt', encoding='utf-8') as f:
    for line in f:
        params = line.split(",")

        # 左手 or 右手
        landmarks = leftLandmarks if params[0] == "0" else rightLandmarks

        # 座標を指定
        landmarks[int(params[1])].location = (float(params[3]),float(params[4]),float(params[5])/1000)
        # 指定した座標、フレームにキーフレームを設定
        landmarks[int(params[1])].keyframe_insert(data_path="location", frame=int(float(params[2])*fps))
4. スクリプトの実行

Editor Type「Text Editor」にスクリプトを貼り付けてRun Script!

5. Animationの再生

Editor Type「Timeline」から再生!

pianoplay.gif

勝ったな:smirk:(フラグ)

6. オブジェクトのエクスポート

File > Export > FBXなどでエクスポートすれば完了!
.blendファイルもUnityでそのまま読み込めます

Blenderでピアノ作成

白鍵と黒鍵をCubeで並べていきます
アニメーションと合わせるので、実物の鍵盤と同じサイズ比を意識します

スクリーンショット 2019-12-18 22.36.19.png

Unityに取り込んで再生

まずは用意した素材を取り込みます

  • アニメーションを適用した3Dモデル
  • ピアノオブジェクト

ピアノの設定

1. 音源Assetのインポート

ここにきて大誤算
ピアノの単音のフリー音源ってあまり無いんですね。。。

自前の電子ピアノから音源を作成しました:weary:

コーラス音源であれば、以下のUnity Assetを無料で利用できます
(同じ販売者から「グランドピアノ音源」も出品されているのですが$50もする・・・)

2. 鍵盤の動きを設定

鍵盤は以下の条件で動かします

  • 動きはy座標回転のみ(xyz位置とxz回転を制限)
  • 指との衝突で回転
  • 天井と床(鍵盤のみと衝突する)を作り、元の位置より上下しないようにする

鍵盤にはRigidbodyとBoxColliderで当たり判定を付与します

スクリーンショット 2019-12-22 5.00.18.png

3. 打鍵すると音が鳴るよう設定

以下の条件で音が鳴るようにしました

  • ハンドオブジェクトと当たり判定がある(OnCollisionStay関数)
  • かつ鍵盤の角度が一定以下

アニメーションの設定

  • アニメーションコントローラの作成

  • 3Dモデルに適用

サイズ比率の調整

手と鍵盤とのサイズ比を調整します
サイズ比や位置の調整用に別途アニメーションを作成しておくとよいかもしれません

今回調整用のアニメーションも作成していたのですが、
手がドリルしていて使えませんでした。。。

再生

雑音が流れました

まとめ

MediaPipe面白い技術ですね!

想像していたよりはきれいにアニメーションしてくれました
ちゃんと調整すればメロディーを奏でそうなムーブではある

簡単な曲 → 複雑な曲というシナリオだったのですが思い通りにいかず

本当は3Dモデルにアニメーションさせたかったのですが、
時間も技術も足りませんでした。。。

アプリのログからアニメーションを作成するという方法もスマートじゃないですね
Linuxで実行できたらもう少しやりようがあったかもしれません
MediaPipe開発者がUnityのサポートについて反応しているので今後に期待ですね

また先日、VRヘッドセットのOculus Questにハンドトラッキングが実装されました
Unity対応のSDKも今月中にリリースとのことなので、Oculus Questをお持ちの方は是非試してみてください
参考:「Oculus Quest」にハンドトラッキング機能実装。コントローラなしでメニューなどの操作が可能に

ちなみに演奏していた曲は「We Wish You a Merry Christmas」でした
よいクリスマスを✨:santa:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FeliCa が遅いしフルスキャンも不可能 後編【iOS 13 Core NFC】

iOS 13 Core NFC での FeliCa に関する制限を紹介した前編、今回はそれの続きです。

iOS での FeliCa の制限

  • FeliCa の読み取りで得られる最初の currentSystemCode は Info.plist での FeliCa システムコードの記述する順番に依存する → 前編で紹介
  • FeliCa の読み取り速度は Info.plist での FeliCa システムコードの記述する順番に依存する → 前編で紹介
  • FeliCa のフルスキャンを iOS で行いたい場合、「じゃあ Info.plist に存在する全ての FeliCa システムコードを記載すればいいのでは…?」と考えつくが、それは現実的な解決策ではない

対応する FeliCa システムコードを増やすと最悪どれくらい遅くなるのか

前編では

iOS 13 の Core NFC の場合は検出された FeliCa カードにあるシステムのうち、Info.plist に記載され 最も順番が前にある FeliCa システムコードに一致するシステム が検出、currentSystemCode および currentIDm にはそれが入ることになります。

そのため、多数の FeliCa システムコードに対応しようとした場合、Info.plist に記載する順番が非常に重要になります。

と記事の最後に述べました。では具体的にどれくらい速度に変化があるのでしょうか。実際に測定してみます。

ここでの "読み取り速度" の定義

そもそも FeliCa は NFC の中で読み取り速度がめちゃ速いです。NFC-B の運転免許証の読み取りに比べても速度が段違いです。

しかし、iOS での Core NFC で私が述べたい "速度" はこのことではありません。

事例紹介をスキップして早く本題に行きたい方はこちらからジャンプ

各 App での読み取り時間に関する表示

iOS 13 のリリース以降、多くの電子マネーカードリーダー系の App が登場しましたが、各 App のスキャン画面等にこんな記載がありませんか?

Japan NFC Reader ICリーダー CardPort
IMG_6487.jpg IMG_6486.jpg IMG_6485.jpg
読み取りに時間がかかる場合があります。 読み込みまで少し時間がかかることがあります。 読み取りには時間がかかることがあります。

(個人的)ド定番 iOS カードリーダー 御三家のどれもに 読み取りに時間がかかる という記載があります。
本記事を読んでいただくと、なぜわざわざこのように表示しているのかがわかり、また文脈としては ICリーダー さんの 読み込みまで少し時間がかかることがある が最も正しい表現であることがわかると思います。

Japan NFC Reader の場合、このあと次のような表示になります。

読み取り中…
…ここで思うことはありませんか?別に「読み取り中…」の文言が表示されるなら、読み取り開始前の画面にわざわざ「読み取りに時間がかかる」ことを表示しなくてもいいんじゃないでしょうか。これは各 App 開発者の親切心で……?

……(少なからず親切心もあるでしょうが)それは違います。

コードを見てみる

ここで少しコードを見てみることにしましょう。

var session: NFCTagReaderSession?

// …

func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

// …

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let tag = tags.first!
    session.connect(to: tag) { (error) in
        // …
        session.alertMessage = "カードを読み取っています…"
        // …
    }
}

App がカードのスキャンを行えるようになった後に表示されるメッセージは、scan() の中にある session.alertMessage です。そして、実際にカードを検出し、プログラム側にその情報がやってくるのが tagReaderSession(_:didDetect:) の中の session.alertMessage です。

ここで大きな問題になるのが、FeliCa カードに iPhone を載せてから tagReaderSession(_:didDetect:) が呼ばれるまでに、ものすごく時間がかかる場合があるということです。その条件が 検出された FeliCa カードに存在するシステムが、Info.plist に記載されている FeliCa システムコードの順番の後ろの方にあるとき です。

ここでの読み取り速度とは、tagReaderSession(_:didDetect:) が呼ばれるまでの時間のことを示します。

検証 ~ FeliCa システムコードを増やすとどれだけ遅くなるか ~

今回は以下のコードを使って、Dateの差がどれだけ増えるかを見ていきます。

func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
    self.start = Date()
    print("tagReaderSessionDidBecomeActive(_:)")
}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let elapsedTime = Date().timeIntervalSince(self.start)
    print("elapsed time:", elapsedTime)
    // …
    session.alertMessage = "完了"
    session.invalidate()
}

対象とするカード、今回は楽天Edyを用います。このようにカードを iPhone の NFC 読み取り部をピッタリと置いた状態で Scan ボタンをタップします。

tagReaderSession(_:didDetect:)print でコンソールに出力される elapsed time の値がどれだけ変化するか測定します。

楽天Edy カードが持っている FeliCa システムコードは 0x8B610xFE00 です。Info.plist に記述する 0xFE00 の順番を段々と後ろにしていきます。
スクリーンショット 2019-12-22 2.14.49.png
スクリーンショット 2019-12-22 2.38.40.png

結果


このグラフのもとになった表はページ下部に記載しました。
FeliCa システムコードの順番が1つ後ろにずれると約0.3秒、tagReaderSession(_:didDetect:) が呼ばれるまでの時間が延びることがわかりました。

また、NFCReaderSession には時間制限があるのですが、その45秒となる Item 148 = 149 個目よりも FeliCa システムコードを多くしてしまうと、そもそも tagReaderSession(_:didDetect:) が呼ばれないことがわかりました。

各カードが持っている FeliCa システムコードはそれぞれ異なる場合があるので、1つの App で多数のカードに対応させようとすると、それだけ Info.plist に記載しなければならない FeliCa システムコードの数も増え、順番によってカードが検出されるまでの時間が左右されるのは使う側としてとても不便です。
また、tagReaderSession(_:didDetect:) が呼ばれないとプログラム側からもカードが iPhone に載せられているのかすら分からないため、各 App では「読み込みまで時間がかかる」という記載がスキャン前の段階で表示されることにつながっています。

Info.plist に 存在する全ての FeliCa システムコードを記載するのは現実的な解決策ではない

もし、iOS で FeliCa のフルスキャンを行おうとしたときに、0x0001 から 0xFEFE までの全ての FeliCa システムコードをInfo.plist に載せれば…?と思いつきますが、そもそも検出のみで149個が限界なので、それはいい方法とは言えないという結論になりました。

そもそもなぜ iOS (Apple) は検出にこれほど時間をかけるのでしょうか。
「ショートカット」のオートメーションに「NFC タグ」が使えますが、これは Apple 製ということもあってか FeliCa も即検出、登録することができています。確実にサードパーティのみを締め出しているということになりますが……。FeliCa システムの IDm による切り替えができないのと合わせて、理由がわかりません。

iOS 14…ではさらなる Core NFC の機能開放を希望します…。

結果(表)

"Item x" の列は 0 は 0xFE00 を Item 0 に記述した場合、1 は Item 1 に記述した場合…というような感じです。Item 7 = 8個目で2秒を超え始めています。実用ではもっと早く検出されたほうがいいですよね。

Item x 1 2 3 Ave.
0 0.046976 0.045720 0.045037 0.045911
1 0.349753 0.346604 0.345791 0.347383
2 0.647677 0.649241 0.646586 0.647835
3 0.947898 0.950009 0.952775 0.950227
4 1.250441 1.250477 1.250740 1.250553
5 1.550706 1.551337 1.552101 1.551381
6 1.854309 1.857909 1.855477 1.855898
7 2.156484 2.155968 2.155585 2.156012
8 2.459123 2.457329 2.458836 2.458429
9 2.759697 2.762735 2.758815 2.760416
10 3.058142 3.060277 3.059271 3.059230
15 4.566140 4.568311 4.569674 4.568042
20 6.072544 6.079473 6.078423 6.076813
25 7.583499 7.585000 7.584232 7.584244
30 9.094974 9.094320 9.096275 9.095190
40 12.111050 12.109540 12.105498 12.108696
50 15.123403 15.119864 15.120571 15.121279
100 30.202593 30.200774 30.201501 30.201623
101 30.512402 30.505607 30.501605 30.506538
102 30.811206 30.814952 30.807862 30.811340
103 31.109441 31.106589 31.107631 31.107887
140 42.270938 42.267985 42.274794 42.271239
145 43.787478 43.777525 43.775778 43.780260
148 44.683454 44.679594 44.683043 44.682030
149 NaN
150 NaN
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Variable Fontにまつわるフォントテクノロジー

はじめに

Variable Fontは2016年頃よりグラフィックデザインの現場、とりわけWebフロント方面を中心に話題になりつつありますが、本トピックではmacOSおよびiOSのネイティブ環境でVariable Fontを実装するための技術を紹介します。また、それに先立って私が調べた範囲でのVariable Fontにまつわる歴史を簡単ではありますがご紹介します。
内容はAppleのテクノロジーの視点に寄ります。

Variable Fontとフォント技術の歴史

Variable Fontにまつわるフォント技術の歴史について、私が調査した範囲でまとめます。「Variable Fontを…するAPI」などの方法論だけ捉えるよりも、いろいろな背景が見えてくると思います。

OpenType Variable Font

昨今よく耳にするVariable Font技術は、正確には “OpenType Variable Fonts” と表現するのがおそらくは正しいのでしょう。OpenType 1.8 に実装された同機能はざっくり言うと、フォントファミリーで複数のスタイルをひとまとめにし、それらを動的に切り替えられるようにする仕組みです。例えば一つの.otfファイルのみで複数のウェイト、字幅、字間など各種スタイルを扱い、描画エンジン側からパラメータを変更することができます。

OpenType Variable Fontに関する概要は、こちらの記事(英語)をまず読んでみてください。仕組みや経緯などが詳しく紹介されています。
Introducing OpenType Variable Fonts – John Hudson

また、OpenTypeの仕組みはMicrosoft Typographyで詳しく解説されています。OpenTypeはMicrosoftとAdobeが開発したテクノロジーであることから、商標も含めMicrosoftに多くの情報が集約されています。OpenTypeに限らず、フォント技術に関する情報を求めるならばMicrosoft Typographyをチェックするのがオススメです。

Microsoft Typography – Microsoft

image.png
引用:https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview

TrueType と Apple Advanced Typography

TrueTypeはデジタルフォントの主要形式の一つとして広く普及しています。1990年代初頭、当時のAdobe Systemsが持っていたPostScript Type1テクノロジーへの対抗として、Apple Computerが開発したフォント技術です。背景にはAdobeに対して発生するロイヤリティの回避があったようです。AppleはTrueTypeをMicrosoftにもライセンスしたので、Windowsにおいても標準的なフォント技術の一つとして採用されています。

Appleは現在、TrueTypeをベースとしたApple Advanced Typography (AAT) テクノロジーをmacOSやiOSのフォントレンダリングに採用していますが、アプリケーションのデベロッパーは何か特殊な事情でも無い限りはAATの存在自体をあまり認知することはないでしょう。同技術に触れるには一般にCore Text APIを経由します。
AppleオリジナルのSan Franciscoフォントの配布ページ Fonts for Apple Platforms にはAATへの言及があります。

Fonts for Apple Platformsページのイメージ
引用:Fonts for Apple Platforms

QuickDraw GX / TrueType GX

AATは、Classic Mac OSの時代に使われていたQuickDraw GX / TrueType GXの後継技術に当たり、Mac OS 8.5から正式に採用されています。QuickDraw GXは当時のMac OS向けグラフィックスアーキテクチャですが、その一部としてTrueType技術を拡張するTrueType GXテクノロジーが後に追加されたとのことです。

Apple included full TrueType support in its Macintosh operating system, System 7, in May 1991. Its more recent development efforts include TrueType GX, which extends the TrueType format as part of the new graphics architecture QuickDraw GX for the MacOS. TrueType GX includes some Apple-only extensions to the font format, supporting Style Variations and the Line Layout Manager.
A brief history of TrueType – Microsoft Typography

(Appleは、1991年5月にMacintoshのオペレーティングシステムであるSystem 7に完全なTrueTypeサポートを組み込みました。その後の開発努力には、Mac OS用の新しいグラフィックアーキテクチャQuickDraw GXの一部としてTrueType形式を拡張するTrueType GXがありました。 TrueType GXには、フォント形式に対するApple専用の拡張機能がいくつか含まれており、スタイルバリエーションと行レイアウトマネージャーをサポートしています。)


QuickDraw GX was eventually "killed" with the purchase of NeXT and the eventual adoption of the Quartz imaging model in Mac OS X, but many of its component features lived on and are now standard in the current Macintosh platform; TrueType GX in particular has, with a few tweaks, become a broadly used modern standard in the form of OpenType Variable Fonts.
QuickDraw GX – Wikipedia

(QuickDraw GXは、AppleによるNeXT買収とMac OS XがQuartzアーキテクチャを採用したことにより最終的には殺されましたが、そのコンポーネントの多くは現在のmacOSにも健在です。 特にTrueType GXは、いくつかの調整が加えられ、OpenType Variable Fontsとして広く使われる標準技術になりました。

QuickDraw GXはMac OS X以降はQuartzを中心としたグラフィックスアーキテクチャに置き換わっていますが、TrueTypeやAATなど、当時の設計思想はそのまま受け継がれている部分も見られます。現代のOpenType Variable FontはTrueType GXの設計思想をベースに標準化された技術です。

Font Variation

Font Variation(フォントバリエーション)はVariable Fontを支える技術です。この技術を理解するには、Axis(軸)と呼ばれる概念を把握する必要があります。TrueTypeやOpenTypeではウェイトや文字幅などにバリエーションを持たせることができ、この一つのバリエーションのことをAxisとして数えることになります。ただし細かい仕様に関して、歴史的にはOpenTypeはTrueType GXの発展であるため、Appleのオリジナルの仕様とOpenTypeの仕様とでは似ているようで大きく異なる部分もあるため、注意が必要です。

Weight Axis
出典:https://docs.microsoft.com/ja-jp/typography/opentype/spec/otvaroverview

Variation Axis Tag

OpenTypeのFont Variationで使われる「登録されたVariation Axis Tag」は次の通りです。

Variation Axis tag 名前 意味
wght Weight ウェイト
wdth Width 文字幅
ital Italic イタリック
opsz Optical size オプティカル
slnt Slant Oblique

Registered axis tags – Microsoft Typography

Variable Fontではこれらのタグを使ってパラメータを操作することになります。Variation Axis Tag名を直接、あるいは対応する定数名を指定して、適切なAPIを呼び出してください。AppleプラットフォームのCore Textを使ったVariation Axisの指定方法はこの後で紹介します。

Microsoftによると、OpenTypeにおける登録されたVariation Axis Tagは基本的に上記のものを使うようにし、もしもカスタムのタグを定義する必要があるならばそれも可能とされています。また、カスタムタグの登録を検討することも勧めています。

登録されたAxis Tagのいくつかは、オリジナルであるTrueTypeのVariation Axisから継承されているタグ名のようです。
Font Variations Table – Apple Developer

いくつかのVariation Axis Tagについては、Mozillaのこのページでも詳しく知ることができます。
Variable フォントガイド – MDN

Core Textの力を借り、macOSでVariable Fontを描画

Variable-Font-Mac.gif

ここからは、macOSネイティブアプリケーションやiOSネイティブアプリケーションでVariable Fontを実現するためのCore Text APIを紹介します。今回は上記のgifアニメーションの具合にVariable FontをmacOSネイティブアプリケーションで描画してみます。iOSのサンプルコードは用意できなかったのですが、基幹技術はすべてCore Textに備わっているので、iOSでも同じようなコードで実現できるはずです。
このアプリケーションのソースコードは次のリポジトリで公開しているでよろしければ参考にしてみてください。

https://github.com/usagimaru/Variable-Font-Mac

フォント選び

そもそもフォントがVariable Fontに対応していなければ話になりませんが、今回は技術検証目的であったのであまりここは深掘りしませんでした。サンプルでは次の書体で検証を行いました。

  • システムフォント (San Francisco) …NSFont.systemFont(ofSize: ) で得られます。
  • SFRounded (.SFNSRounded-Regular) …macOSシステムフォントのSFRoundedを直呼び出ししています。あまり良くない方法です。
  • Montserrat
  • Recursive Sans

Variable Font with Hiragino

システムフォントはVariable Fontに対応しています。試してみたところSFRoundedも対応していました。例のgifアニメーションで使っている書体はSFRoundedです。日本語はヒラギノがW0からW9まで揃っているので、システムフォントからのフォールバックでウェイトが連動してくれるようでした。ただ少しだけ描画がおかしな部分も見られました。(私の実装が問題なのかもしれません)
そのほかにもWebで無料配布されていた二つの書体も試していますが、これらも大きな問題なく描画されました。

CTFontCopyVariationAxes(_:)

フォントからVariation Axisに関する情報を取得して、それらを元にして新しいパラメータの値を載せてから、それを再度フォントに反映させるという方針をとります。

Core TextのCTFontCopyVariationAxesからは、フォントが備えるVariation AxisのIdentifierや名前、デフォルト値、最大値、最小値等に関する情報を取得できます。

public func CTFontCopyVariationAxes(_ font: CTFont) -> CFArray?

// Swift では [[String : Any]] にキャストしておくと楽かも

このメソッドは、引数としてCTFontオブジェクトを与えると、そのフォント(ファイル)が持つVariation Axisの情報を次のような形式で返してくれます。型はCFArray(配列)です。Core TextなのでCFやCTのプレフィックスを多く見かけますが、適宜キャストすることでSwiftでも扱いやすくなります。

CTFontCopyVariationAxesで返るデータのダンプ
(
    {
        NSCTVariationAxisDefaultValue = 400;
        NSCTVariationAxisIdentifier = 1196572996;
        NSCTVariationAxisMaximumValue = 1000;
        NSCTVariationAxisMinimumValue = 400;
        NSCTVariationAxisName = GRAD;
    },
    {
        NSCTVariationAxisDefaultValue = 400;
        NSCTVariationAxisIdentifier = 2003265652;
        NSCTVariationAxisMaximumValue = 1000;
        NSCTVariationAxisMinimumValue = 1;
        NSCTVariationAxisName = Weight;
    }
)

NSCTVariationAxis… が並びますが、これらはドキュメントには載っていない値なので次の定数 (CFString) を使ってください。

  • kCTFontVariationAxisIdentifierKey : Axisの固有ID、フォントが違っても同じ意味のAxisなら同じ値になるようです。
  • kCTFontVariationAxisDefaultValueKey : このAxisのデフォルト値です。
  • kCTFontVariationAxisMinimumValueKey : Axisの最小値です。ウェイトなら一番細い値。
  • kCTFontVariationAxisMaximumValueKey : Axisの最大値です。
  • kCTFontVariationAxisNameKey : Axis名です。“Weight”など。
  • kCTFontVariationAxisHiddenKey : AxisをアプリケーションのUIに見せたくない場合に、フォントデザイナーがtrueを指定することがあります。(オプショナル)

Font Variation Axis Dictionary Keys – Apple Developer

Sample1
if let axes = CTFontCopyVariationAxes(font as CTFont) as? [[String : Any]] {
    for axis in axes {
        let axisIdentifier = axis[kCTFontVariationAxisIdentifierKey as String] as? Int
        let axisName = axis[kCTFontVariationAxisNameKey as String] as? String
        let axisDefaultValue = axis[kCTFontVariationAxisDefaultValueKey as String] as? CGFloat
        let axisMinimumValue = axis[kCTFontVariationAxisMinimumValueKey as String] as? CGFloat
        let axisMaximumValue = axis[kCTFontVariationAxisMaximumValueKey as String] as? CGFloat
        let axisHidden = axis[kCTFontVariationAxisHiddenKey as String] as? Bool
    }
}

CTFontDescriptorCreateCopyWithVariation

カスタマイズしたVariation Axisをフォントに反映するには、CTFontDescriptorCreateCopyWithVariationを使います。このメソッドは、元となるフォントデスクリプタ (CTFontDescriptor) に対し、変更したいVariation AxisのIdentifer変更したいVariation Axisに反映する値をそれぞれ引数で与えます。そうすると変更された新たなフォントデスクリプタのコピーが得られます。

public func CTFontDescriptorCreateCopyWithVariation(_ original: CTFontDescriptor, _ variationIdentifier: CFNumber, _ variationValue: CGFloat) -> CTFontDescriptor

このメソッドでは一度に変更できるVariation Axisの数は一つですから、同時に複数のVariation Axisを変更したい場合は、例えば次の具合にチェーン形式に処理する必要がありそうです。

Sample1
let originFontDescriptor: CTFontDescriptor =  // 元となるフォントデスクリプタ
let fontAxes =  // Variation Axisごとに変更値を収めた連想配列
var nextFontDescriptor: CTFontDescriptor = originFontDescriptor

// Axisを更新
for fontAxisKey in fontAxes.keys {
    guard let fontAxis = fontAxes[fontAxisKey] else { continue }

    let axisIdentifier = NSNumber(value: fontAxis.identifier) as CFNumber
    let axisValue = fontAxis.customValue
    nextFontDescriptor = CTFontDescriptorCreateCopyWithVariation(nextFontDescriptor,
                                                                 axisIdentifier,
                                                                 axisValue)
}

if let newFont = NSFont(descriptor: nextFontDescriptor, size: self.fontSize) {
    // NSFontを更新(iOSではUIFont)
    self.textView.font = newFont
}

このようにしてフォントから抽出したVariation Axisのデフォルト値をもとに、適当なパラメータを反映させてから CTFontDescriptorCreateCopyWithVariation で変更をフォントに反映すれば、めでたくmacOS / iOSネイティブアプリケーションでVariable Fontを描画することができます。最終的に画面描画される部分はNSFont (UIFont) を更新する処理となるため、グラフィックスコンテクストをいじったりなど特殊な描画処理を書く必要はありません。
(ただ、フォントに都度変更を加える処理がどれだけパフォーマンスに影響を与えるのかまでは深く検証できていないため、場合によってはグラフィックスの描画処理を見直す必要があるかもしれません。)

少し概念が特殊ではあるのですが、それほど難しいものでもないため、案外簡単にネイティブアプリケーションでVariable Fontを実現できてしまいます。使いどころはそう多くないとは思いますけれども、何かユースケースがあれば検討してみてください。そのためだけにWebViewを使うみたいな方法を取らずに済むでしょう。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSのショートカットの自動化レシピ

はじめに

iOS12からiOSのショートカットが導入されました。iOSのショートカットが導入された当初は1週間ほどいじって遊んでいたのですが、それ以降全然触っていませんでした。

iOS13.1では、オートメーション機能が導入され、NFCタグをトリガーとしたショートカットを作成できるなど便利になっています。私が持っているiPhone XS端末はNFCタグに未対応かと思って、オートメーション機能を使うのを断念していたのですが、実は対応していることを知り、今年の12月頃から活用方法がないかあれこれ触り始め始めました。

ここでは、あれこれ触る中で学んだiOSのショートカットの活用方法をレシピとして紹介したいなと思います。

前提

まずは、iOSのショートカットを使うには、iOSのショートカットに対応したiOS12以上が必要になります。また、オートメーション機能を使用するにはiOS13.1以上である必要があります。

その他、NFCタグのオートメーション機能を使うには、以下のいずれかのiPhone端末が必要です。

  • iPhone 11
  • iPhone XS
  • iPhone XR

本記事では、iPhone XSのiOS13.3で検証を行なっています。

レシピ集

ここでは、実際に作成して試したものや思いついたものについて、レシピという形式で紹介していきたいと思います。

ショートカットでメニューを表示する

概要

メニューでショートカットを選択して実行できるようにします。
これで何が良いかというと、ウィジェットからメニューに表示されたショートカットを選択することで、アプリを起動するのが簡単になります。

準備

  • ショートカットアプリ
  • メニュー表示のショートカット作成

手順

新規ショートカット作成画面を開き、スクリプティングのメニューから選択を追加します。

image.png

上記で表示されたプロンプトにメニューに表示する項目(例えばアプリ名など)を追加します。ここでは、認証アプリ(Google Autheticator、Microsoft Authenticator、1password)を追加します。

※ 既に作成済みのショートカットで解説しているため、画像には、認証とショートカット名が表示されています。

image.png

メニューの項目を選択したときに実行する動作を追加します。ここでは、スクリプティングのアプリを開くを追加し、アプリ名を実行するアプリに変更します。

image.png

ナビゲーションバーの次へを押下し、ショートカット名を入力することで完了です。

動作

ウィジェットから、認証のショートカットを選択することで、3つのアプリがメニューに表示され、アプリを素早く起動することができます。
利用シーンとしては、Githubなどの2段階認証のコードが必要なページで、認証コードをアプリからコピペしたい時や、1passwordなどに対応していないアプリでパスワードをコピペしたい時などに役に立つのではないでしょうか。

image.pngimage.png

応用

メニュー表示のショートカットは、他にもいろいろ活用できるかと思います。
例えば、漫画アプリをまとめるとか、今流行りの乱立したxPayあたりのアプリをまとめておくと、レジとかで素早く起動できて便利な気もします。

二段階認証のQRコードをバックアップする

概要

二段階認証のQRコードをバックアップしたい時ってありませんか?

認証コードをアプリに保存したはいいけれども、モバイル端末を機種変更した時にバックアップを取ってなくて、再度二段階認証の解除と再設定を行なったことがありました。そのときに激しくめんどくさかったので、二段階認証コードの発行時にQRコードをカメラで撮影して、それをEvernoteのクラウド上にバックアップするという手順で運用するようになりました。

ただ、カメラで撮影すると、上手く撮影されていなく、いざというときに利用できないことも考えられるので、これを上手いこと自動化したいというのが、今回のレシピになります。

準備

  • ショートカットアプリ
  • メニュー表示のショートカット作成
  • Evernoteアプリ

手順

簡単に流れを説明すると、対象のQRコードをカメラでスキャン読み取ったコードからQRコードの画像を生成生成した画像を写真アプリに保存写真アプリから最新の画像を取得Evernoteに取得した画像を任意のノートに追加 になります。

生成した画像を写真アプリに保存は無駄では?と思うと思いますが、なぜかEvernoteに直接画像を追加できなかったため、苦肉の策として写真アプリに生成した画像を保存・取得という無駄なステップを追加しています。

それでは、レシピの詳細になります。

新規ショートカット作成画面を開き、アクションからQR/バーコードをスキャンを追加します。

image.png

アクションからQRコードを生成を追加します。

image.png

写真から写真アルバムに保存を追加します。

image.png

写真から最新の写真を取得を追加します。

image.png

アプリのEvernoteに追加を追加します。入力は、最新の写真を選択します。
テキストは、ここでは先頭に 二段階認証_のテキストを追加し、残りのテキストは実行時にテキスト入力するようにしています。最後に、バックアップ対象の任意のノートを選択します。

image.pngimage.png

最後に、スクリプティングからアプリを開くを追加し、Evernoteアプリを開くようにします。こちらのステップは特に必須ではないですが、バックアップが正しくされたかを確認するために、アプリを起動しています。

手順は、以上になりますが、全体のスクリーンショットを載せておきます。

image.pngimage.png

動作

ウィジェットから二段階認証のバックアップのショートカットを選択します。
image.png

コードスキャンの画面が起動するので、二段階認証用のQRコードを読み取ります。
ここでは、TwitterのQRコードを使用しています。

image.png

Evernoteのダイアログが表示されるので、任意の名称を追加します。
ここでは、Twitterを追加しています。
image.png

最後に、Evernoteが起動し、二段階認証_Twitterの名称でQRコードが保存されていることを確認できます。
image.png

応用

ここでは、二段階認証をテーマに説明しましたが、通常のQRコードにも活用できるかと思います。
また、バックアップの保存先もEvernote以外を選択しても良いですし、自分のメールアドレスに送ることもできるのではないかと思います。

タイマーを自動開始する

概要

NFCタグをトリガーにタイマーを自動起動します。
カップラーメンの3分タイマーや筋トレのタイマーなどに役に立つのではと思います。

準備

  • ショートカットアプリ
  • NFCタグ (対応端末含む)
  • オートメーションのショートカット作成

手順

ショートカットアプリのオートメーションを開き、オートメーションを新作成します。ここでは、個人用オートメーションを作成を選択します。

image.png
新規オートメーション画面でNFCを選択します。
image.png
スキャンを選択し、使用したいNFCタグを読み込みます。
image.png

NFCタグを読み込むと、名称編集用のダイアログが現れるので、任意の名称を追加します。ここでは、3分タイマーとしています。
image.png

次に、アクション選択画面が表れるので、まずは Appから時計アプリを選択し、タイマーを開始を追加します。

image.png

ここでは、タイマーを3分に設定します。
image.png

次に、タイマーがNFCタグが読み込まれたことをわかりやすくするために、アクションからデバイスを振動させるを追加します。これは、必須ではありませんが入れておきます。

image.png

次に、これも必須ではないですが、時計アプリを起動するアクションを追加します。スクリプティングからアプリを開くを追加し、時計アプリを選択します。

image.png

最後に、ナビゲーションバーの次へボタンを選択し、実行の前に尋ねるをオフにした上で、完了ボタンを選択します。

image.png

これで、NFCタグのオートメーションを使ったタイマーが完成です。

動作

実際にNFCタグを読み取った動作のgifになります。時計アプリの起動後はタイマータブに自動的に切り替わらないないため、ここだけは手動で切り替えています。

image.png

応用

タイマーの時間をあらかじめ調整したり、タイマーの時間を実行時に指定することでいろいろなケースにタイマーを活用できるかと思います。

特定のアイテムに関連したアプリを起動する

概要

アイテムにNFCタグを貼っておくことで、アイテムに関連したアプリを素早く起動します。
例えば、体組成計で体重を計測する際に、体組成計アプリを立ち上げる → 体組成計に乗って計測をはじめるといった動作をよく行います。ここで問題になるのが、体組成計アプリを探して起動することに時間がかかってしまうことがあることです。この問題を解決するために、体組成計アプリ自体にNFCタグを貼り付けておいて、NFCタグを読み込むことで体組成計アプリを起動するようにします。

準備

  • ショートカットアプリ
  • 体組成計
  • 体組成計アプリ
  • NFCタグ (対応端末含む)
  • オートメーションのショートカット作成

手順

まずは、体組成計にNFCタグを貼り付けておきます。

次に、タイマーを自動開始するで解説した時と同じように、新規オートメーションを作成し、 NFCタグをスキャンします。

その後、スクリプティングのアクションを開き、体組成計アプリを起動するように設定します。

image.png

今回のレシピは、アプリを起動するだけであるため、手順は単純ですね。

応用

NFCタグを読み取って、体組成計アプリを起動するというレシピは一例で、他にいろんなアイテムにNFCタグを貼り付けて、関連するアプリを起動するというのは応用が効くのではないかと思います。

例えば、

  • 銀行カードにNFCタグを貼り付けて、関連する銀行アプリを起動するようにする。
  • 低温調理器具(例えばAnova)にNFCタグを貼り付けて、Anovaアプリを起動する
  • 社員証に組み込まれているNFCタグを読み取って、入退室管理アプリを起動する。

などなど、アイテムやコンテキストをトリガーとしたショートカットを作成するのは、おもしろいかもしれませんね。

スマートロックの解錠・施錠

概要

スマートロックを導入したドアをSiriショートカットを使って、解錠・施錠できるようにします。
Siriショートカットは、Siriの音声コマンドでショートカットを実行できるのですが、ウィジェット経由で呼び出したり、NFCタグをトリガーに呼び出すこともできます。

このレシピでは、スマートロックのSesameを使った例について解説します。

image.png

準備

手順

SesameのスマートロックとWifiモジュールをセットアップ済みの前提で解説します。

Sesameを使ったショートカットアプリはあらかじめSiriショートカットが定義されているため、比較的に簡単なのですが、Siriショートカットをどこで追加するのか比較的悩みました。

Siriショートカットを追加するには、ショートカットアプリのギャラリータブを選択し、お使いAppからショートカットの全て表示を選択し、お使いAppからショートカット画面にセサミのSiriショートカットが配置されています。
※ ただし、たまに表示されないため、このあたりのロジックがどうなっているかが正直わかりません。

image.pngimage.png

ここで、「home」を解錠「home」を施錠を選択し、Siriに追加することで、Siriショートカットとして呼び出すことができるようになります。

動作

ウィジェットから解錠用のSiriショートカットを動作させた時は、次のようになります。

RPReplay_Final1576945820 2 3.gif

ウィジェットから施錠用のSiriショートカットを動作させた時は、次のようになります。

RPReplay_Final1576945856.gif

応用

NFCタグのオートメーション経由でSesameのSiriショートカットを呼び出すことで、NFCタグを読み取るだけで解錠・施錠できるようになります。
※ NFCタグのオートメーション自体は、設定した端末でのみ有効なため、他人がNFCタグを勝手に読み込んで解錠・施錠されるような心配はありません。

例えば、ドアの前部分にNFCタグを貼り付けておいて、すぐに解錠ができるようにしています。
ただし、Wifiモジュールとの通信ラグが結構発生し、5秒から10秒くらい待たされるので、急いでいる時はSesameのアプリから操作した方が、解錠・施錠ともに速いかもしれません。

image.png

Google Assistant経由で機器を操作する

概要

Google AssistantをSiriショートカットから呼び出し、Google Assistant経由でGoogle Homeを操作します。例えば、Google HomeでSpotifyの音楽を流したり、Nature Remoに対してエアコンをONにする操作ができたりします。

準備

  • ショートカットアプリ
  • Google Assistantアプリ
  • Google Home
  • ショートカットの作成

手順

Google Assistantアプリは、Siriショートカットに対応していため、このSiriショートカットをウィジェットから呼び出すようにします。

このGoogle AssitantアプリのSiriショートカットですが、Sasame同様にSiriショートカットをどこから追加すれば良いか分からず結構悩みました。

Siriショートカットを追加するには、どうやらGoogle Assistant上で一度検索を行う必要があるようで、検索を行なった後に、ショートカットアプリを見てみると、追加するアプリやギャラリー上に、Google AssistantのSiriショートカットが表示されるようになりました。

ここでは、Google Assistant経由でGoogle HomeでSpotifyを再生する手順を解説します。
ただし、Google HomeとSpotifyが既に連携済みであることと、Google AssistantとGoogle Homeの連携ができていることを前提とします。

まずは、Google Assistantアプリを起動し、アプリに対して Google HomeでSpotifyを再生してのように音声で指示を行います。この動作で、Google HomeからSpotifyの音楽が再生できれば、OKです。

IMG-2018.PNG

次に、ショートカットアプリを起動し、アクション追加画面でAppを選択し、Assistantを探します。

image.png

Assistantを選択し、先ほど音声で指示を出したGoogle HomeでSpotifyを再生してのショートカットがあることを確認し選択します。

image.png

次の画面で、実行に表示をOFFにし、次ヘボタンを選択します。
ショートカット名には任意の名称を追加します。ここでは、Google HomeでSpotifyを再生としました。最後に、完了ボタンを選択します。

image.pngimage.png

これで、マイショートカットに作成したショートカットが表示されるので、ウィジェットからもショートカットを呼び出せるようになります。

動作

マイショートカット画面からGoogle HomeでSpotifyを再生のショートカットを起動したgif画像です。

RPReplay-Final1576949647.gif

応用

ウィジェット経由でGoogle AssitantのSiriショートカットを呼び出すことで音声を出さずとも指示を出すことができて便利です。これも、NFCタグのオートメーションと組み合わせるといろいろと活用できるのではないでしょうか。

SwitchBotでエントランスを解錠

概要

SwitchBotを活用して、エントランスの自動解錠を行います。
インターフォンを押した後に、IFTTT経由でSwitchBotに指示を出し、SwitchBotにインターフォンの応答と解錠を代行してもらいます。

※ ちなみに、このレシピはタイミングが大事で、ネットワークの遅延などでタイミングが合わずによく失敗するため、十分に活用できていません。普通に鍵を使った方が速いかもしれません。まあ、やってみたかっただけです。

準備

  • ショートカットアプリ
  • SwitchBot x 2台
  • SwitchBot Hub
  • IFTTT
  • IFTTTのアクションの作成
  • ショートカットの作成

手順

まずは、SwitchBotを準備し、インターフォンのボタンを押す用のSwitchBotと解錠ボタンを押す用のSwitchBotを用意します。

image.png

SwitchBotとIFTTTの中継用にSwitchBot Hubを準備します。

image.png

SwitchBotとSwitchBot HubをSwitBotアプリ経由で接続する必要がありますが、ここでは省略します。
SwitchBotの公式サイトを参照してください。

次にIFTTTのアクション設定を行います。
まずは、プロフィールボタンからCreateを選択します。

image.png

次に Thisを選択し、Choose a service画面でWebHooksのサービスを選択します。

image.png

Choose trigger画面で、Recieve a web requestを選択します。その後、
Complete trigger fieldEvent Nameを設定します。ここでは、インターフォンのボタンを押す用のinterphoneを指定しました。設定後、Create triggerボタンを押下します。

image.png

次に Thatを選択します。Thatでは、SwitchBotとの接続設定を行います。

image.png

Choose action service画面で SwitchBotのアクションを選択します。

image.png

Choose a action画面でたくさんのアクションが表示されますが、Bot pressを選択します。

image.png

次に、Complete action field画面では、接続するSwitchBotのデバイスを選択します。
画像では2つのデバイスが選択されていますが、インターフォンのボタンを押す用のSwitchBotを指定します。

image.png

これで、インターフォンのボタンを押す用のSwitchBotの設定は完了ですが、同様に 解錠ボタンを押す用のSwitchBotの設定を行います。ここでは、手順が同じため詳細は省略します。

image.png

上記で作成したIFTTTのWebHookは以下のURLに対してリクエストを投げることで実行されます。

$ curl -X POST https://maker.ifttt.com/trigger/{event}/with/key/{自分のAPIキー}

ここからは、ショートカットを作成します。

新規ショートカット作成画面を開き、URLの内容を取得のアクションを選択します。
このURLにインターフォンのボタンを押す用のWebHookのURLを指定します。
ここでは、https://maker.ifttt.com/trigger/interphone/with/key/{自分のAPIキー}を指定します。

この後、特に必須ではないですが、デバイスを振動させるアクションを追加しています。
※ 実際に処理が進んでいるか確認するためのデバッグ用途です。

image.png

次に、もうひとつ、URLの内容を取得のアクションを選択します。
このURLに解錠ボタンを押す用のWebHookのURLを指定します。
ここでは、https://maker.ifttt.com/trigger/unlock/with/key/{自分のAPIキー}を指定します。

ここでも同様に、デバイスを振動させるアクションを追加しています。

image.png

2つ目のAPIのWebhookを叩いた後に、スクリプティングから 待機アクションを追加して、10秒待機します。
※ この待機アクションを追加しているのは、次の理由のためです。
IFTTT経由でSwitchBotと連携する際に、インターフォンのボタンを押す解錠ボタンを押すの2つのWebHookのイベントを作成したのですが、これらのイベントのAPIに対して連続でAPIを叩くと、順番が前後したりする可能性があったため、注意が必要です。
例えば、インターフォンのボタンを押す解錠ボタンを押すインターフォンのボタンを押すのAPIを順番に叩くと、インターフォンのボタンを押すインターフォンのボタンを押す解錠ボタンを押すのように処理が前後する可能性があります。このバグ?にハマって時間を無駄に使ってしまいました。
一時的な解決策として今回は、インターフォンのボタンを押す解錠ボタンを押すの後に、数秒遅延させてインターフォンのボタンを押すを最後に実行するようにしました。

最後に、URLの内容を取得のアクションを追加し、インターフォンのボタンを押す用のWebHookのURLを指定します。これは、インターフォンの終話を目的としています。

image.png

インターフォーンのボタンを押す箇所のボタン部分はなかなかSwitchBotと相性が悪く、SwitchBotが完全にボタンを押し込めない状態になることがありました。対策として、クッションを間に挟むことで押し込む動作を補強するようにしました。

image.png

動作

随分わかりにくいですが、SwitchBot Hubにリクエストがいくと、SwithBot Hubが点滅し、SwitchBotが動作します。一瞬、赤いランプが光る箇所は、解錠ボタンが押された時になります。

※このgif画像では、3番目のAPIリクエストの動作部分がなぜか切れて写っておりません。

IMG_0002 3 2.gif

応用

今回のレシピでは、エントランスの自動解錠にSwitchBotを活用しましたが、インターネット経由(あるいは、ローカルネットワーク)で物理スイッチを押したいケースにいろいろと活用できるのではないかと思います。

SSH接続でサーバに処理をさせる

概要

ショートカット経由で、SSH接続しサーバーに対して任意のコマンドを実行します。
ここでは、サーバ上で単純にdateコマンドを実行し、サーバー日時を表示します。

準備

  • ショートカットアプリ
  • SSH接続用のサーバー

手順

新規ショートカット作成画面を開き、スクリプティングからSSH経由でスクリプトを実行を選択します。

image.png

接続先の情報を設定します。ここで注目したいのは、iOS12では SSH接続でパスワード認証する必要があったのですが、iOS13.1から公開鍵認証ができるようになっています。
ここでは、公開鍵認証を使用し、認証項目でSSHキーを選択します。
公開鍵をサーバー上に追加するには、SSHキー項目を選択すると、画面が開くので、この画面で新しいキーを生成するとともに、公開鍵を共有から任意の場所に公開鍵を配布することができるようになります。公開鍵をサーバーに追加する方法については省略します。

image.png

次にサーバの実行結果を表示するようにします。実行結果を表示するには、クイックルック通知を表示などいろいろ方法がありますが、ここでは通知を表示アクションを追加します。

image.png

これで、今回のレシピは完了になります。

動作

image.png

応用

SSH接続して任意のコマンドを実行できるので、いろんなことができそうですね。
私が試してみたのは、サーバ上にヘッドレスChromeのpuppeteerをインストールして、自分が住んでいる地域の天気をWebページのDOMをスクリーンショットに保存して画像を表示させたり、同様にGithubの草画像のスクリーンショットを保存して画像を表示するようなことをしました。

その他、外出先から簡単にサーバーの起動・再起動・停止をショートカットのメニュー選択経由で実行してみるのも利便性が高い様な気もします。

今後、ラズベリーパイにSSH接続して任意の機器を操作するようなことも試してみたいと思っています。

情報共有に活用する。

概要

ショートカットは情報共有の際にも活用しています。
気になった記事をTwitterやSlackに投稿したいこともありますよね?
私はTwitterを投稿すると同時に、Slackのチャンネルも同時に共有したいなーと思った時にこのレシピのショートカットを使用しています。

ここでは、記事の共有シートからTwitterとSlackに投稿する方法について紹介します。

準備

  • ショートカットアプリ
  • Twitterアプリ
  • Slackアプリ
  • SlackのチームIDと投稿先のチャンネルID
  • ショートカットの作成

手順

まずは、記事から共有するための記事のタイトルとリンクを取得します。

作成するショートカットでは、共有する記事のリンクを受け入れる必要があるため、ショートカットアプリのそのための準備として、ギャラリータブにある共有のためのショートカットを選択し、Twitterでリンクを検索をショートカットに追加します。

image.pngimage.png

このショートカットをベースに変更を加えていきます。

image.png

また、ショートカットの名称を任意に変更します。ここでは、twislakに変更します。

image.png

まずは、受け入れるの項目をURLだけ選択し、それ以外の項目は一旦全て削除します。

image.png

次に変数を設定のアクションを追加し、変数をURLにします。

image.png

URLのタイトルを取得するために、名前を取得アクションを追加します。

image.png

次に、テキストアクションを追加し、取得したタイトルをもとに、共有するテキストを作成します。
ここでは、"タイトル" URLの形式でテキストを作成しています。

image.png

※ 実際に上記の共有するテキストが作成できているかを確認するために、一度、クイックルックアクションを追加して、動作確認してみると良いかと思います。

これで、共有するテキストの準備が整ったので、まずはTwitterの投稿のアクションを追加します。

image.png

これで、Twitterの投稿の設定は完了になります。
次は、Slackの投稿の設定を行なっていきます。

Slackの投稿を行うには、いくつか方法があるかと思います。

  • Slackのショーカットを使用する。(これは使用したことがないのでテキストの共有ができるかはわかりません。というよりも、さっきこのショートカットがギャラリーにあることを知りました。)
  • SlackのAPIを使用する。
  • SlackのURLスキームで指定のチャンネルを開き、手動で投稿する。

理想では、SlackのショートカットやSlackのAPIで投稿することでTwitterとの同時投稿が実現できるように思えますが、ここではSlackのURLスキームで指定のチャンネルを開き、手動で投稿するを実現したいと思います。

ここで検討する必要があるのは、Slackの任意のチャンネルを開くURLスキームの仕様が何であるかです。
これは、まあSlackの公式ページのDeep linking into Slack clientsに記載されているのですが、
slack://channel?team={TEAM_ID}&id={CHANNEL_ID}の形式になっています。
つまり、TEAM_IDCHANNEL_IDを調べて、構築したURLスキームのURLをショートカットアプリで開けば、Slackのアプリが起動し、指定のチャンネルへの自動遷移ができるということです。

ただし、自分のワークスペースのSlackのTEAM_IDCHANNEL_IDはすぐに分かる気がしますが、外部のワークスペースのTEAM_IDCHANNEL_IDを調べる方法はあるのでしょうか??

もしかするとすぐに分かる方法があるかもしれませんが、私は‎Charles ProxyのiOSアプリでパケットキャプチャすることで調べることができました。詳細については、割愛しますが、Slackアプリを開いて、指定のチャンネルを開いた時のリクエストURLなどを参照することで特定できるかと思います。

Slackの投稿設定でずいぶんと長くなりましたが、ようやくSlackの投稿ができるようになりました。

その前に、Slackで手動投稿するためのテキストをクリップボードにコピーしておきます。
そこで、クリップボードにコピーアクションを追加します。

image.png

ここから、Slackアプリの指定のチャンネルに移動する設定になります。

URLスキーム設定用のURLアクションを追加し、slack://channel?team={TEAM_ID}&id={CHANNEL_ID}の形式の文字列を追加します。

image.png

最後に、URLを開くアクションを追加して完了になります。

image.png

動作

作成したtwislakのショートカットを使用して、気になる記事のTwitter投稿とSlack投稿を行なったgif画像になります。最後に手動でペーストするのは若干手間ですが、他の方法も模索していきたいと思います。

RPReplay-Final1576950909.gif

応用

今回のレシピでは、URLスキームの起動を活用しました。他のアプリにあるURLスキームやSiriショートカットを調べることで、便利なショートカットが実現できるかもしれません。

さいごに

ここで紹介したレシピはほんの一部でまだまだ活用方法はたくさんあると思います。

  • Pythonの実行環境のPythonistaアプリと連携する
  • 任意のAPIを叩く
  • 任意のWebページでJavaScriptを実行する
  • SSH接続で外部サーバーと連携する

など、いろんなことができるかと思います。これからも、活用方法を見つけていたきたいと思います。
それでは、自分なりのショートカットでいろいろ自動化して楽しみましょう!enjoy!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARでマンガを覗き読み【iOS】

はじめに

AR、いいですよね。素敵な世界。
マンガ、いいですよね。素敵な世界。

ということで、今回はiOSのARKitを利用して、紙のマンガを覗き読みできるアプリ
nekonote1-compressor.gif

を実装してみました。

デモに利用させていただいているマンガは、
マンガボックスで連載中の「ネコの手、借りてます。」?
作家の遥那もより様から許可をいただき動画掲載させていただいてます???
最高オブ最高の癒しマンガなのでみなさん是非読んでください???

いつでも、どこでも、だれもが、この素敵な世界を実現できるよう、
実装を、コメント多めで紹介します✨

前準備

1.お高めなiPhone(A9以降のプロセッサを搭載したもの)を購入?します

2.プロジェクトを作ります
[ File > New > Project > Single View App ]

3.Storyboard に ARSCNView を貼り付け、ViewController と紐付けます

4.カメラ利用の許可を取るために、Info.plistに追加します
[ Privacy - Camera Usage Description ]

5.Assets.xcassetsを開き、[ New AR Resource Group ]を追加。
フォルダの中にマーカーとして検出したい画像(今回はマンガの表紙)を入れます。
スクリーンショット 2019-12-21 21.36.21.png

この時、画像の名前現実世界での大きさを入力します。
マーカーにふさわしい画像についてはこちらを参考にしてください。

6.ARオブジェクトとして表示したい画像(今回はマンガの原稿)を[ New Image Set ]で追加します。

実装

1.設定

ARImageTrackingConfigurationを設定します。

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController {

    @IBOutlet weak var sceneView: ARSCNView!

    var session: ARSession {
        return sceneView.session
    }

    let updateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".serialSceneKitQueue")

    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView.delegate = self
        resetTracking()
    }

    func resetTracking() {
        // Assetsの読み込み
        guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
            fatalError("Missing expected asset catalog resources.")
        }

        // 既知の2D画像を追跡するconfigの設定
        let configuration = ARImageTrackingConfiguration()
        configuration.trackingImages = referenceImages
        // ARオブジェクトの手前に人が映り込む時、オクルージョン処理してくれる設定
        configuration.frameSemantics = .personSegmentation

        // session開始
        // .resetTracking: デバイスの位置をリセットする
        // .removeExistingAnchors: 配置したオブジェクトを取り除く
        session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // session停止
        sceneView.session.pause()
    }
}

configuration.frameSemantics = .personSegmentationは、
ARKit3から追加されたピープルオクルージョンの設定で、設定するとこのように

nekonote2-compressor.gif

人の指が原稿の手前にくるので、マンガの中を覗き読んでいる!という世界をよりリアルに実現できます。
ただ、まだ少し精度が甘くチリチリすることは否めないので、お好みでオフにして下さい?

2.マーカーを検知してオブジェクトを表示

表紙を検知し、ARSCNViewDelegateで通知を受け取ったら原稿を表示します。

// MARK: - ARSCNViewDelegate
extension ViewController: ARSCNViewDelegate {
    // 新しいARアンカーに対応するノードが追加されたことを通知
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let imageAnchor = anchor as? ARImageAnchor else {
            return
        }
        // 表示したいオブジェクトをARアンカーの名前から決定
        let objectImage: UIImage
        switch imageAnchor.referenceImage.name {
        case "nekonote" :
            objectImage = nekonotes[nekonoteIndex]
        case "hanakaku":
            objectImage = hanakakus[hanakakuIndex]
        default:
            return
        }

        updateQueue.async {
            // sceneにノードを追加
            node.addChildNode(self.createNode(image: objectImage, name: imageAnchor.referenceImage.name ?? "no name"))
        }
    }

    private func createNode(image: UIImage, name: String) -> SCNNode {
        // 検出されたARアンカーの位置を視覚化する平面に合わせて、長方形のノード(SCNPlane)を作成
        let scale: CGFloat = 0.2
        let plane = SCNPlane(width: image.size.width * scale / image.size.height,
                             height: scale)
        // firstMaterial:平面の最初のマテリアル
        // diffuse: 表面から拡散反射される光の量、拡散光はすべての方向に等しく反射されるため、視点に依存しない、contentsに画像をset
        plane.firstMaterial?.diffuse.contents = image
        let planeNode = SCNNode(geometry: plane)
        planeNode.name = name
        // SCNPlaneはローカル座標空間で垂直方向を向いているが、ARアンカーは画像が水平であると想定しているため
        // 一致するように回転させる
        planeNode.eulerAngles.x = -.pi / 2
        // アニメーション
        planeNode.position = SCNVector3(0.0, 0.0, -0.15)
        planeNode.scale = SCNVector3(0.1, 0.1, 0.1)
        planeNode.runAction(self.imageAction)
        return planeNode
    }
}

3.アニメーション

ARはかっこよく表示したい、ですよね、ですよね。
ということで、はじめに検知した時にフェードアニメーションを追加します?

    var imageAction: SCNAction {
        return .sequence([
            .scale(to: 1.5, duration: 0.3),//スケール
            .scale(to: 1, duration: 0.2),
            .fadeOpacity(to: 0.8, duration: 0.5),//フェード
            .fadeOpacity(to: 0.1, duration: 0.5),
            .fadeOpacity(to: 0.8, duration: 0.5),
            .move(to: SCNVector3(0.0, 0.0, 0.0), duration: 1),//移動
            .fadeOpacity(to: 1.0, duration: 0.5),
        ])
    }

.sequenceで連続したアニメーションを処理することができます。
センスがないとか言わないで?

4.タップ

画面をタップでページをめくりましょう。

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard
            let location = touches.first?.location(in: sceneView),
            let result = sceneView.hitTest(location, options: nil).first else {
                return
        }
        // ノードの名前を取得し画像変更
        let node = result.node
        let objectImage: UIImage
        switch node.name {
        case "nekonote" :
            nekonoteIndex += 1
            objectImage = nekonotes[nekonoteIndex % 4]
        case "hanakaku":
            hanakakuIndex += 1
            objectImage = hanakakus[hanakakuIndex % 4]
        default:
            return
        }
        // アニメーションしながら画像差し替え
        node.runAction(self.pageStartAction, completionHandler: {
            node.geometry?.firstMaterial?.diffuse.contents = objectImage
            node.runAction(self.pageEndAction, completionHandler: nil)
        })
    }

    var pageStartAction: SCNAction {
        return .fadeOpacity(to: 0.0, duration: 0.1)
    }
    var pageEndAction: SCNAction {
        return .fadeOpacity(to: 1.0, duration: 0.2)
    }

完成?

nekonote3-compressor.gif

これで、本屋さんでマンガに封がしてあっても、中身を覗き読みすることができちゃいますね?

サンプルアプリはこちらに公開しています✨
koooootake/MangaTrialAR-ios

動画はこちら

もうひと作品お借りしたのは個人利用可能で作品を公開してくださっている「ハナカク」様?
とても絵が綺麗いいい本当にありがとうございますすす✨

おわりに

やりたいこと

ここから、マンガのコマをVisionで分解してアニメーションさせたり
指先のジェスチャーを検知してページをめくったりして、現実を拡張したいいい

AppleのARグラスが売り出され?
コンテンツの表現が紙やディスプレイに留まらず拡張される素敵な世界が楽しみです?

おまけ

DeNAアドベントカレンダー終盤を担当したので
この記事を読んだ、ものづくり好きな人にオススメの記事を勝手に紹介?

【3日で実装・公開】エモいアートな画像生成アプリ開発
エモい!!!じんむのアイコンもエモくなりました

【Electron+GCP+Slack App】Slackのコメントをニコニコ動画風にプレゼンで流す方法
「Slack」のコメントというところが、社会人には超超超実用的✨

退職者を送る技術 - Twilio と Socket.IO で作る電話マルチプレイシステムの小ネタ
電話を掛けて、キーパットを利用して、みんなで操作するゲームの作り方?発想良すぎぎぎ

来年もべしべしものづくり楽しもううう?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む