- 投稿日:2019-12-22T23:44:49+09:00
AVAudioEngineでリモートの音楽ファイルを再生する
やりたいこと
リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいのでAVAudioEngineを使う。
環境
- Xcode: 11.1
- iOS: 13.1.2
- リポジトリ: https://github.com/fuziki/RemoteAudioPlayerNode
手順
- 音楽ファイルをダウンロードする
- パケットに分割する
- パケットをPCMに変換する
- PCMをAVAudioEngineを使って再生する
実装
step1. 音楽ファイルをダウンロードする
URLSessionを使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるようにRemoteAudioDownloaderのprotocolを定義。RemoteAudioDownloader.swiftpublic 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.swiftpublic 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.swiftextension 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.swiftextension 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.swiftlet 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を生成する
CompressedBufferConverter.swiftlet pcmBuff = AVAudioPCMBuffer(pcmFormat: dstFormat, frameCapacity: frames)! pcmBuff.frameLength = pcmBuff.frameCapacitystep3-3. AVAudioConverterを使って変換する
AVAudioConverterを使って変換する。
CompressedBufferConverter.swiftconverter.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.swiftengine.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
- 投稿日:2019-12-22T21:33:10+09:00
【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])使い方
OptionSet
とSet<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
で実装する
- 投稿日:2019-12-22T18:47:27+09:00
SkyWayでiOS/Androidアプリを作るときの勘所
メリークリスマス
(まだ早い)
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といろんなユースケースと向き合う必要があるので気合い入れていこうな。
- 投稿日:2019-12-22T17:12:57+09:00
UITableViewCellのボタンをどのCellのものか判別する
自分へのリマインドを主として書いています。
分かりにくかったら申し訳ないです。UITableViewCellにボタンを追加
- addTargetを追加
sample.swiftfunc 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.swiftfunc 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
- 投稿日:2019-12-22T16:51:27+09:00
動的解析ツール 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-trace
やfrida
で解析していきます。ここからは実際に
FLEXing
という脱獄Tweak(脱獄することで入れられる機能やアプリのことをTweakと呼びます)がどのような動作をしているか、fridaを駆使して解明していこうと思います!その前にFLEXingについて軽く説明します。
FLEXingとは
FLEX
というiOS用のデバッグツールを、任意のiOSアプリケーション上で起動できるようにするTweakです。仕組みは簡単で、任意のアプリケーションに
libFLEX.dylib
(FLEX
の本体)というダイナミックライブラリをinject(注入)するだけです。画像. 時計アプリにinjectされた
FLEX
実際にやってみる
- 投稿日:2019-12-22T14:51:06+09:00
Vision.frameworkでカメラ画像のテキスト認識を行う
前回の記事では、
Vision.framework
をつかって顔認識を行いました。
今度はテキスト認識をやってみます。
ちなみに、テキストの文字認識はiOS13からの機能みたいです。概要
カメラ画像からテキストを検出し、テキスト部分に矩形を表示。
さらにその部分に検出したテキストを出力します。現在のところ、対応言語が英語のみのようです。
また、今回のサンプルでは端末を横にしないと、文字をうまく認識しません。試した環境
Xcode 11.3
iOS 13.2
swift 5実行サンプル
iOS Vision.framework を使った テキスト検出実験https://t.co/vtmH4Uuywt
— becky (@beckyJPN) December 22, 2019動画なので速度を出すため、検証精度を落として確認しています。
画面をアップにするとそこそこの精度は出ていそうです。検証に使ったサイトのURLは以下です。
https://en.wikipedia.org/wiki/Appleコード説明
手順的には、顔認証とほぼ同じで、リクエストを
VNDetectFaceRectanglesRequest
からVNRecognizeTextRequest
に変更します。
VNRecognizeTextRequest
で画像から検出した文字情報を[VNRecognizedTextObservation]
として受け取ります。
ちなみに、VNDetectTextRectanglesRequest
でも文字の矩形取得はできるのですが、こちらの場合文字情報を取得することができません。また、リクエストにプロパティを設定することで、文字取得条件を変更できます。
recognitionLevel
= 文字の取得制度設定。fast
とaccurate
があり、動画で検出する場合は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参考サイト
- iOS13から標準サポートされる文字認識
https://qiita.com/KenNagami/items/a75b2bc282ad05a6dcde- Core Text で縦書き - 錯綜
https://hrt1ro.hatenablog.com/entry/2018/09/27/132803- 【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】 - Qiita
https://qiita.com/beckyJPN/items/4bc46a8e6a000b711de6
- 投稿日:2019-12-22T14:46:37+09:00
FlutterでiOSアプリ開発入門(1/-)
FlutterでiOSアプリ開発入門
本記事はmacOSユーザーを想定しています。
ほとんどGoogleが提供するドキュメントの直訳です?なぜFlutterなのか
- 技術的資産性が高い
- メンテナンスされている
- 学習コストが低い
Googleによるメンテナンス、発展の余地、クロスプラットフォーム、ホットリロードなど様々な要素から技術的な資産性が高いな〜と感じています
またβ版のFlutter for Webも気になります…!
【第一部】導入編
こちらのドキュメントに従ってインストールしていきます
https://flutter.dev/docs/get-started/install/macosGet started
ステップ1と2はgitからcloneすることで省略可能
$ git clone https://github.com/flutter/flutter.git1. Download the Flutter SDK
Get the Flutter SDKからSDKをダウンロードします
https://flutter.dev/docs/get-started/install/macos2. Locate the file
解凍したファイルを任意のディレクトリに置きます(ドキュメントではDevelopmentというディレクトリに設置)
$ mkdir ~/Development $ cd ~/Development $ unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip3. Add the flutter tool to your path
.zshrcに以下を記載
export PATH="$PATH:`pwd`/Development/flutter/bin"sourceで反映
source .zshrc4. (Optional) Pre-download development binaries
一応ダウンロードしておきます。
$ flutter precache5. Run flutter doctor
Flutterがインストールされていることを確認します。
$ flutter doctorFlutterのインストールが確認できました。
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 -runFirstLaunch2. Set up the simulator
iOSデバイスのシュミレーターを立ち上げます
$ open -a Simulator3. 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 setupb. Open Runner
RunnerのプロジェクトがXcodeで開きます
open ios/Runner.xcworkspacec. Setup Xcode
XcodeはUIがめちゃくちゃわかりにくいので気を強く持ってください
- Runnerを選択
- Signing & Capabilitiesを選択
- Teamを設定
- Bundle Identifierを設定
このあたりは他の記事に説明を譲ります
d. Launch app in real iPhone
sample_app が追加されています!
第一部
完
- 投稿日:2019-12-22T14:46:37+09:00
FlutterでiOSアプリ開発入門(第一部)
FlutterでiOSアプリ開発入門
本記事はmacOSユーザーを想定しています。
ほとんどGoogleが提供するドキュメントの直訳です?なぜFlutterなのか
- 技術的資産性が高い
- メンテナンスされている
- 学習コストが低い
Googleによるメンテナンス、発展の余地、クロスプラットフォーム、ホットリロードなど様々な要素から技術的な資産性が高いな〜と感じています
またβ版のFlutter for Webも気になります…!
【第一部】 導入編
こちらのドキュメントに従ってインストールしていきます
https://flutter.dev/docs/get-started/install/macosGet started
ステップ1と2はgitからcloneすることで省略可能
$ git clone https://github.com/flutter/flutter.git1. Download the Flutter SDK
Get the Flutter SDKからSDKをダウンロードします
https://flutter.dev/docs/get-started/install/macos2. Locate the file
解凍したファイルを任意のディレクトリに置きます(ドキュメントではDevelopmentというディレクトリに設置)
$ mkdir ~/Development $ cd ~/Development $ unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.5-stable.zip3. Add the flutter tool to your path
.zshrcに以下を記載
export PATH="$PATH:`pwd`/Development/flutter/bin"sourceで反映
source .zshrc4. (Optional) Pre-download development binaries
一応ダウンロードしておきます。
$ flutter precache5. Run flutter doctor
Flutterがインストールされていることを確認します。
$ flutter doctorFlutterのインストールが確認できました。
?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 -runFirstLaunch2. Set up the simulator
iOSデバイスのシュミレーターを立ち上げます
$ open -a Simulator3. 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 setupb. Open Runner
RunnerのプロジェクトがXcodeで開きます
open ios/Runner.xcworkspacec. Setup Xcode
XcodeはUIがめちゃくちゃわかりにくいので気を強く持ってください
- Runnerを選択
- Signing & Capabilitiesを選択
- Teamを設定
- Bundle Identifierを設定
このあたりは他の記事に説明を譲ります
d. Launch app in real iPhone
sample_app が追加されています!
第二部に続く?
(coming soon)
- 投稿日:2019-12-22T14:30:01+09:00
TextFieldタップでDataPickerを呼ぶ
下図のように編集開始時に
DatePicker
を起動させ,時間変更時に変更時間をTextFieldに反映させます.
ViewController.swiftvar 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() }
- 投稿日:2019-12-22T14:30:01+09:00
TextFieldタップでDatePickerを呼ぶ
下図のように編集開始時に
DatePicker
を起動させ,時間変更時に変更時間をTextFieldに反映させます.
まずTextFieldのDelegateを追加します.
ViewController.swiftclass ViewController: UIViewController, UITextFieldDelegate次に以下のコードを追加hします
ViewController.swiftvar 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() }
- 投稿日:2019-12-22T13:51:23+09:00
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
条件は
if
やswitch
で、ループはfor-in
やwhile
、repeat-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")
person
とday
が引数にあたるラベルであり、それぞれのラベルのコロンの後の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?.sideLengthEnumerations 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列の要素それぞれの値は、先頭から
0
、1
、2
...というように順番に割り当てられる。
(例:case a, b, c
という列なら、a
には0
が、b
には1
、c
には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
mutating
はstruct
やenum
において自身の値を変更する場合に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
最後に
先人の知恵を勝手にたくさん拝借しました。ありがとうございます。
(リンク先の記事がどれもわかりやすく、理解に際して非常にありがたかったです…)修正や参考資料などございましたらご指定いただけますと幸いです。
ご覧くださいましてありがとうございました。
- 投稿日:2019-12-22T08:30:48+09:00
【2019年12月版】mitmproxyでアプリの通信内容を確認したい
アプリ開発初心者なので
先日、自社で作成したアプリの最終チェックに関わったんですが、その時に「通信内容見たいな~」って思ったんですよ。まあサーバ側が見れればそこで見ればいいんですけど、昨今のアプリはいろんなところと通信するので、すべての通信内容を見れなかったりする事情もあるわけで。なので、そういう時に気軽に通信内容をチェックできるMan-In-The-Middleなproxyを立ててみようと思いました。
mitmproxy
ググると一発目でこいつに行き当たりました。ふーん、Fiddlerじゃないんだ。
Qiitaにも当然のように記事がたくさんあったので、まずは入れてみて動かしてみましょう。導入手順
WSL導入
動作環境はWindows 10なので、まずはWSLを有効化します。
スタートメニューを右クリック->アプリと機能を選択。
再起動が求められたりするので、立ち上がったらWindows Storeでubuntuを探しましょう。
mitmproxyのインストール
mitmproxyの導入はaptのおかげで至極簡単です。
sudo apt install python3-pip && sudo pip3 install -U pip && sudo pip3 install mitmproxymitmproxyの起動と端末側の準備
SHIFT + Fでfollowモード(tailみたいな感じ)にしておきましょう。
これでmitmproxyが起動しましたが、準備はまだ終わっていません。端末側にproxyの設定と、mitmproxyの証明書をインストールする必要があります。
まずはproxyの設定。ポート指定してない場合は8080です。
次に証明書です。
端末側でブラウザを開き、mitm.itとアドレスに打ち込みます。
利用する端末のアイコンを選択して、証明書をダウンロードします。
iOSの場合、これだけでは証明書が有効にならないようです。
設定を開くとプロファイルのインストールがアラートとして出てくるので、それを選択します。最後に、設定 > 一般 > 情報 > 証明書信頼設定で有効にしておく必要があります。
使ってみる。
アプリも見てましょう。例としてsmartnewsを開いた時の通信を見てみました。
いい感じです。
見れないアプリもある。
証明書のpinning(これ、なんて訳すんでしょうね)という方法で中間者攻撃を無効化するというのは、比較的いろいろなアプリで実施されているようです。
先ほどsmartnewsの通信を垣間見ましたが、yahoo newsでは通信を見ることができません。証明書のpinningに対応しているものと思われます。
同様に、以前は通信内容を見ることができたAmazonなどのアプリも、中間者攻撃に対して対策がされているようで、mitmproxy動作下では正常に動作しないようです。
対策がされているアプリとそうでないアプリを調べていくのもなかなか楽しそうです。
逆に言うと、自分たちが作るアプリでも同様のことをしなくてはいけないケースを想定しなくてはいけませんね。
- 投稿日:2019-12-22T08:06:07+09:00
スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった
この記事は、 North Detail Advent Calendar 2019 の22日目の記事です
概要
やりたいこと
- ピアノ演奏中の手のモーションをスマホカメラで記録する
- Unityでアニメーションしてピアノ演奏する
技術的なこと
- google/mediapipeでハンドトラッキング用のiOSアプリをビルド
- iPhoneカメラで手のモーションを記録
- Blenderでアニメーション化 & ピアノオブジェクト作成
- Unityに取り込んで再生、指と鍵盤の当たり判定で音を鳴らす
成果物
※雑音が流れるのでご注意を
Unityでピアノを弾いてみたかった
— ヤマト (@yamatohkd) December 21, 2019
(雑音が流れますのでご注意を) pic.twitter.com/fVuHP9Xb0K......はい(出落ち)
作業環境
・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など)やメディア処理機能など、モジュラーコンポーネントのグラフとして認識パイプラインを構築できます。
......なるほど
(?)
どうやら機械学習パイプライン(画像処理→モデル推論→描画 など)の構築や、
それをグラフとして視覚化できるフレームワークらしいサンプルでは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をインストール
Xcodeと
Command Line Tools
をインストールPythonバージョンの確認
私の環境ではPython 2.7ではビルドが通らなかったため、下記サイトを参考にPython 3.7.5
をインストールしました
参考:pyenvを使ってMacにPythonの環境を構築する"six"ライブラリをインストール
$ pip install --user future six2. MediaPipeリポジトリをクローン
$ git clone https://github.com/google/mediapipe.git $ cd mediapipe3. 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 bazel4. OpenCVとFFmpegをインストール
こちらも二通りあるので
Option 1
の方法でインストール$ brew install opencv@35. 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 DesktopAndroidの場合
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.apkiOSの場合
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/BUILD
45行目のbundle_idを、
Provisioning Profileで設定したBundle Identifier
に変更mediapipe/examples/ios/multihandtrackinggpu/BUILD:45bundle_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ファイルをインストールすれば完了です!手の動きをトラッキング
ビルドしたアプリを動かしてみる
おお!両手の動きがリアルタイムで反映されていますね
このときのiPhoneのプロセス状態を、USB接続したMacの
コンソール
アプリで確認してみましょう
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空間に浮かべてみます
なぁにこれぇ?ログをよくみるとz座標だけ数値が異様に大きいですね
倍率を変更してみます
おっ!なんかそれっぽくなった!
向きと大きさを調整して線で繋いでみると・・・
完全に手だコレ! \\ ٩( 'ω' )و//あとはピアノを演奏しているところを撮影して座標を取得するだけです
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座標
2. Landmarkオブジェクトの作成
以下のSphereオブジェクトを追加
これをLandmarkポイントとして動かします
- Left.000 ~ Left.020
- Right.000 ~ Right.020
3. スクリプトの作成
Pythonスクリプトはこんな感じ
投稿時間を過ぎてしまったので説明は省略...
(倍率とか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」から再生!
勝ったな
(フラグ)
6. オブジェクトのエクスポート
File > Export > FBXなどでエクスポートすれば完了!
.blendファイルもUnityでそのまま読み込めますBlenderでピアノ作成
白鍵と黒鍵をCubeで並べていきます
アニメーションと合わせるので、実物の鍵盤と同じサイズ比を意識しますUnityに取り込んで再生
まずは用意した素材を取り込みます
- アニメーションを適用した3Dモデル
- ピアノオブジェクト
ピアノの設定
1. 音源Assetのインポート
ここにきて大誤算
ピアノの単音のフリー音源ってあまり無いんですね。。。自前の電子ピアノから音源を作成しました
コーラス音源であれば、以下のUnity Assetを無料で利用できます
(同じ販売者から「グランドピアノ音源」も出品されているのですが$50もする・・・)2. 鍵盤の動きを設定
鍵盤は以下の条件で動かします
- 動きはy座標回転のみ(xyz位置とxz回転を制限)
- 指との衝突で回転
- 天井と床(鍵盤のみと衝突する)を作り、元の位置より上下しないようにする
鍵盤にはRigidbodyとBoxColliderで当たり判定を付与します
3. 打鍵すると音が鳴るよう設定
以下の条件で音が鳴るようにしました
- ハンドオブジェクトと当たり判定がある(OnCollisionStay関数)
- かつ鍵盤の角度が一定以下
アニメーションの設定
アニメーションコントローラの作成
3Dモデルに適用
サイズ比率の調整
手と鍵盤とのサイズ比を調整します
サイズ比や位置の調整用に別途アニメーションを作成しておくとよいかもしれません今回調整用のアニメーションも作成していたのですが、
手がドリルしていて使えませんでした。。。再生
雑音が流れました
まとめ
MediaPipe面白い技術ですね!
想像していたよりはきれいにアニメーションしてくれました
ちゃんと調整すればメロディーを奏でそうなムーブではある簡単な曲 → 複雑な曲というシナリオだったのですが思い通りにいかず
本当は3Dモデルにアニメーションさせたかったのですが、
時間も技術も足りませんでした。。。アプリのログからアニメーションを作成するという方法もスマートじゃないですね
Linuxで実行できたらもう少しやりようがあったかもしれません
MediaPipe開発者がUnityのサポートについて反応しているので今後に期待ですねまた先日、VRヘッドセットのOculus Questにハンドトラッキングが実装されました
Unity対応のSDKも今月中にリリースとのことなので、Oculus Questをお持ちの方は是非試してみてください
参考:「Oculus Quest」にハンドトラッキング機能実装。コントローラなしでメニューなどの操作が可能にちなみに演奏していた曲は「We Wish You a Merry Christmas」でした
よいクリスマスを✨
- 投稿日:2019-12-22T05:18:21+09:00
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 読み取りに時間がかかる場合があります。 読み込みまで少し時間がかかることがあります。 読み取りには時間がかかることがあります。 (個人的)ド定番 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:)
のelapsed time
の値がどれだけ変化するか測定します。楽天Edy カードが持っている FeliCa システムコードは
0x8B61
と0xFE00
です。Info.plist に記述する0xFE00
の順番を段々と後ろにしていきます。
結果
このグラフのもとになった表はページ下部に記載しました。
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
- 投稿日:2019-12-22T04:26:17+09:00
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
引用:https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverviewTrueType と 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への言及があります。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の仕様とでは似ているようで大きく異なる部分もあるため、注意が必要です。
出典:https://docs.microsoft.com/ja-jp/typography/opentype/spec/otvaroverviewVariation 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 フォントガイド – MDNCore Textの力を借り、macOSでVariable Fontを描画
ここからは、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に対応しています。試してみたところ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
Sample1if 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を変更したい場合は、例えば次の具合にチェーン形式に処理する必要がありそうです。
Sample1let 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を使うみたいな方法を取らずに済むでしょう。
参考資料
- Introducing OpenType Variable Fonts – John Hudson
- About Apple Advanced Typography Fonts – Apple Developer
- Apple Advanced Typography – Wikipedia
- Microsoft Typography – Microsoft Typography
- A brief history of TrueType – Microsoft Typography
- QuickDraw GX – Wikipedia
- QuickDraw GX – Wikiwand
- TrueType GX Model Lives On in OpenType Variable Fonts – Ata Distance
- The State of OpenType Variable Fonts vs. Advanced Typography Feature Fragmentation – Ata Distance
- Fonts on Macintosh – Wikiwand
- OpenType – Wikipedia
- OpenTypeの機能とよくある質問 – Adobe Type
- Frequently asked questions – Adobe Type
- What's the difference between opentype vs. truetype – Graphic Design Stack Exchange
- Font Variations Table – Apple Developer
- Variable フォントガイド – MDN
- Zapfino Extra detail – Linotype
- CJKV日中韓越情報処理 – Ken Lunde(原著)/小松 章(訳)/逆井 克己(訳)
- CTFontDescriptorCreateCopyWithVariation - Core Text – Apple Developer
- CTFontCopyVariation(_:) - Core Text – Apple Developer
- Font Variation Axis Dictionary Keys – Apple Developer
- 投稿日:2019-12-22T03:59:07+09:00
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で検証を行なっています。
レシピ集
ここでは、実際に作成して試したものや思いついたものについて、レシピという形式で紹介していきたいと思います。
ショートカットでメニューを表示する
概要
メニューでショートカットを選択して実行できるようにします。
これで何が良いかというと、ウィジェットからメニューに表示されたショートカットを選択することで、アプリを起動するのが簡単になります。準備
- ショートカットアプリ
- メニュー表示のショートカット作成
手順
新規ショートカット作成画面を開き、スクリプティングの
メニューから選択
を追加します。上記で表示されたプロンプトにメニューに表示する項目(例えばアプリ名など)を追加します。ここでは、認証アプリ(Google Autheticator、Microsoft Authenticator、1password)を追加します。
※ 既に作成済みのショートカットで解説しているため、画像には、
認証
とショートカット名が表示されています。メニューの項目を選択したときに実行する動作を追加します。ここでは、スクリプティングの
アプリを開く
を追加し、アプリ名を実行するアプリに変更します。ナビゲーションバーの
次へ
を押下し、ショートカット名を入力することで完了です。動作
ウィジェットから、認証のショートカットを選択することで、3つのアプリがメニューに表示され、アプリを素早く起動することができます。
利用シーンとしては、Githubなどの2段階認証のコードが必要なページで、認証コードをアプリからコピペしたい時や、1passwordなどに対応していないアプリでパスワードをコピペしたい時などに役に立つのではないでしょうか。応用
メニュー表示のショートカットは、他にもいろいろ活用できるかと思います。
例えば、漫画アプリをまとめるとか、今流行りの乱立したxPayあたりのアプリをまとめておくと、レジとかで素早く起動できて便利な気もします。二段階認証のQRコードをバックアップする
概要
二段階認証のQRコードをバックアップしたい時ってありませんか?
認証コードをアプリに保存したはいいけれども、モバイル端末を機種変更した時にバックアップを取ってなくて、再度二段階認証の解除と再設定を行なったことがありました。そのときに激しくめんどくさかったので、二段階認証コードの発行時にQRコードをカメラで撮影して、それをEvernoteのクラウド上にバックアップするという手順で運用するようになりました。
ただ、カメラで撮影すると、上手く撮影されていなく、いざというときに利用できないことも考えられるので、これを上手いこと自動化したいというのが、今回のレシピになります。
準備
- ショートカットアプリ
- メニュー表示のショートカット作成
- Evernoteアプリ
手順
簡単に流れを説明すると、
対象のQRコードをカメラでスキャン
→読み取ったコードからQRコードの画像を生成
→生成した画像を写真アプリに保存
→写真アプリから最新の画像を取得
→Evernoteに取得した画像を任意のノートに追加
になります。※
生成した画像を写真アプリに保存
は無駄では?と思うと思いますが、なぜかEvernoteに直接画像を追加できなかったため、苦肉の策として写真アプリに生成した画像を保存・取得という無駄なステップを追加しています。それでは、レシピの詳細になります。
新規ショートカット作成画面を開き、アクションから
QR/バーコードをスキャン
を追加します。アクションから
QRコードを生成
を追加します。写真から
写真アルバムに保存
を追加します。写真から
最新の写真を取得
を追加します。アプリの
Evernoteに追加
を追加します。入力は、最新の写真
を選択します。
テキストは、ここでは先頭に二段階認証_
のテキストを追加し、残りのテキストは実行時にテキスト入力するようにしています。最後に、バックアップ対象の任意のノートを選択します。最後に、スクリプティングから
アプリを開く
を追加し、Evernote
アプリを開くようにします。こちらのステップは特に必須ではないですが、バックアップが正しくされたかを確認するために、アプリを起動しています。手順は、以上になりますが、全体のスクリーンショットを載せておきます。
動作
ウィジェットから
二段階認証のバックアップ
のショートカットを選択します。
コードスキャンの画面が起動するので、二段階認証用のQRコードを読み取ります。
ここでは、TwitterのQRコードを使用しています。Evernoteのダイアログが表示されるので、任意の名称を追加します。
ここでは、
最後に、Evernoteが起動し、
二段階認証_Twitter
の名称でQRコードが保存されていることを確認できます。
応用
ここでは、二段階認証をテーマに説明しましたが、通常のQRコードにも活用できるかと思います。
また、バックアップの保存先もEvernote以外を選択しても良いですし、自分のメールアドレスに送ることもできるのではないかと思います。タイマーを自動開始する
概要
NFCタグをトリガーにタイマーを自動起動します。
カップラーメンの3分タイマーや筋トレのタイマーなどに役に立つのではと思います。準備
- ショートカットアプリ
- NFCタグ (対応端末含む)
- オートメーションのショートカット作成
手順
ショートカットアプリのオートメーションを開き、オートメーションを新作成します。ここでは、
個人用オートメーションを作成
を選択します。
新規オートメーション
画面でNFC
を選択します。
スキャンを選択し、使用したいNFCタグを読み込みます。
NFCタグを読み込むと、名称編集用のダイアログが現れるので、任意の名称を追加します。ここでは、3分タイマーとしています。
次に、アクション選択画面が表れるので、まずは
App
から時計アプリを選択し、タイマーを開始
を追加します。次に、タイマーがNFCタグが読み込まれたことをわかりやすくするために、アクションから
デバイスを振動させる
を追加します。これは、必須ではありませんが入れておきます。次に、これも必須ではないですが、時計アプリを起動するアクションを追加します。スクリプティングから
アプリを開く
を追加し、時計アプリを選択します。最後に、ナビゲーションバーの次へボタンを選択し、
実行の前に尋ねる
をオフにした上で、完了ボタンを選択します。これで、NFCタグのオートメーションを使ったタイマーが完成です。
動作
実際にNFCタグを読み取った動作のgifになります。時計アプリの起動後はタイマータブに自動的に切り替わらないないため、ここだけは手動で切り替えています。
応用
タイマーの時間をあらかじめ調整したり、タイマーの時間を実行時に指定することでいろいろなケースにタイマーを活用できるかと思います。
特定のアイテムに関連したアプリを起動する
概要
アイテムにNFCタグを貼っておくことで、アイテムに関連したアプリを素早く起動します。
例えば、体組成計で体重を計測する際に、体組成計アプリを立ち上げる
→体組成計に乗って計測をはじめる
といった動作をよく行います。ここで問題になるのが、体組成計アプリを探して起動することに時間がかかってしまうことがあることです。この問題を解決するために、体組成計アプリ自体にNFCタグを貼り付けておいて、NFCタグを読み込むことで体組成計アプリを起動するようにします。準備
- ショートカットアプリ
- 体組成計
- 体組成計アプリ
- NFCタグ (対応端末含む)
- オートメーションのショートカット作成
手順
まずは、体組成計にNFCタグを貼り付けておきます。
次に、
タイマーを自動開始する
で解説した時と同じように、新規オートメーションを作成し、 NFCタグをスキャンします。その後、スクリプティングの
アクション
を開き、体組成計アプリ
を起動するように設定します。今回のレシピは、アプリを起動するだけであるため、手順は単純ですね。
応用
NFCタグを読み取って、体組成計アプリを起動するというレシピは一例で、他にいろんなアイテムにNFCタグを貼り付けて、関連するアプリを起動するというのは応用が効くのではないかと思います。
例えば、
- 銀行カードにNFCタグを貼り付けて、関連する銀行アプリを起動するようにする。
- 低温調理器具(例えばAnova)にNFCタグを貼り付けて、Anovaアプリを起動する
- 社員証に組み込まれているNFCタグを読み取って、入退室管理アプリを起動する。
などなど、アイテムやコンテキストをトリガーとしたショートカットを作成するのは、おもしろいかもしれませんね。
スマートロックの解錠・施錠
概要
スマートロックを導入したドアをSiriショートカットを使って、解錠・施錠できるようにします。
Siriショートカットは、Siriの音声コマンドでショートカットを実行できるのですが、ウィジェット経由で呼び出したり、NFCタグをトリガーに呼び出すこともできます。このレシピでは、スマートロックのSesameを使った例について解説します。
準備
- ショートカットアプリ
- Sesameのスマートロック
- SesamiのWifiモジュール
- Sesamiアプリ
- ショートカットの作成
手順
SesameのスマートロックとWifiモジュールをセットアップ済みの前提で解説します。
Sesameを使ったショートカットアプリはあらかじめSiriショートカットが定義されているため、比較的に簡単なのですが、Siriショートカットをどこで追加するのか比較的悩みました。
Siriショートカットを追加するには、ショートカットアプリのギャラリータブを選択し、お使いAppからショートカットの
全て表示
を選択し、お使いAppからショートカット画面にセサミのSiriショートカットが配置されています。
※ ただし、たまに表示されないため、このあたりのロジックがどうなっているかが正直わかりません。ここで、
「home」を解錠
と「home」を施錠
を選択し、Siriに追加することで、Siriショートカットとして呼び出すことができるようになります。動作
ウィジェットから解錠用のSiriショートカットを動作させた時は、次のようになります。
ウィジェットから施錠用のSiriショートカットを動作させた時は、次のようになります。
応用
NFCタグのオートメーション経由でSesameのSiriショートカットを呼び出すことで、NFCタグを読み取るだけで解錠・施錠できるようになります。
※ NFCタグのオートメーション自体は、設定した端末でのみ有効なため、他人がNFCタグを勝手に読み込んで解錠・施錠されるような心配はありません。例えば、ドアの前部分にNFCタグを貼り付けておいて、すぐに解錠ができるようにしています。
ただし、Wifiモジュールとの通信ラグが結構発生し、5秒から10秒くらい待たされるので、急いでいる時はSesameのアプリから操作した方が、解錠・施錠ともに速いかもしれません。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です。次に、ショートカットアプリを起動し、アクション追加画面で
App
を選択し、Assistant
を探します。Assistantを選択し、先ほど音声で指示を出した
Google HomeでSpotifyを再生して
のショートカットがあることを確認し選択します。次の画面で、実行に表示をOFFにし、次ヘボタンを選択します。
ショートカット名には任意の名称を追加します。ここでは、Google HomeでSpotifyを再生
としました。最後に、完了ボタンを選択します。これで、マイショートカットに作成したショートカットが表示されるので、ウィジェットからもショートカットを呼び出せるようになります。
動作
マイショートカット画面から
Google HomeでSpotifyを再生
のショートカットを起動したgif画像です。応用
ウィジェット経由でGoogle AssitantのSiriショートカットを呼び出すことで音声を出さずとも指示を出すことができて便利です。これも、NFCタグのオートメーションと組み合わせるといろいろと活用できるのではないでしょうか。
SwitchBotでエントランスを解錠
概要
SwitchBotを活用して、エントランスの自動解錠を行います。
インターフォンを押した後に、IFTTT経由でSwitchBotに指示を出し、SwitchBotにインターフォンの応答と解錠を代行してもらいます。※ ちなみに、このレシピはタイミングが大事で、ネットワークの遅延などでタイミングが合わずによく失敗するため、十分に活用できていません。普通に鍵を使った方が速いかもしれません。まあ、やってみたかっただけです。
準備
- ショートカットアプリ
- SwitchBot x 2台
- SwitchBot Hub
- IFTTT
- IFTTTのアクションの作成
- ショートカットの作成
手順
まずは、SwitchBotを準備し、
インターフォンのボタンを押す
用のSwitchBotと解錠ボタンを押す
用のSwitchBotを用意します。SwitchBotとIFTTTの中継用にSwitchBot Hubを準備します。
SwitchBotとSwitchBot HubをSwitBotアプリ経由で接続する必要がありますが、ここでは省略します。
SwitchBotの公式サイトを参照してください。次にIFTTTのアクション設定を行います。
まずは、プロフィールボタンからCreate
を選択します。次に
This
を選択し、Choose a service
画面でWebHooksのサービスを選択します。
Choose trigger
画面で、Recieve a web request
を選択します。その後、
Complete trigger field
でEvent Name
を設定します。ここでは、インターフォンのボタンを押す
用のinterphone
を指定しました。設定後、Create trigger
ボタンを押下します。次に
That
を選択します。That
では、SwitchBotとの接続設定を行います。
Choose action service
画面でSwitchBot
のアクションを選択します。
Choose a action
画面でたくさんのアクションが表示されますが、Bot press
を選択します。次に、
Complete action field
画面では、接続するSwitchBotのデバイスを選択します。
画像では2つのデバイスが選択されていますが、インターフォンのボタンを押す
用のSwitchBotを指定します。これで、
インターフォンのボタンを押す
用のSwitchBotの設定は完了ですが、同様に解錠ボタンを押す
用のSwitchBotの設定を行います。ここでは、手順が同じため詳細は省略します。上記で作成した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キー}
を指定します。この後、特に必須ではないですが、
デバイスを振動させる
アクションを追加しています。
※ 実際に処理が進んでいるか確認するためのデバッグ用途です。次に、もうひとつ、
URLの内容を取得
のアクションを選択します。
このURLに解錠ボタン
を押す用のWebHookのURLを指定します。
ここでは、https://maker.ifttt.com/trigger/unlock/with/key/{自分のAPIキー}
を指定します。ここでも同様に、
デバイスを振動させる
アクションを追加しています。2つ目のAPIのWebhookを叩いた後に、スクリプティングから
待機
アクションを追加して、10秒待機します。
※ この待機アクションを追加しているのは、次の理由のためです。
IFTTT経由でSwitchBotと連携する際に、インターフォンのボタンを押す
と解錠ボタンを押す
の2つのWebHookのイベントを作成したのですが、これらのイベントのAPIに対して連続でAPIを叩くと、順番が前後したりする可能性があったため、注意が必要です。
例えば、インターフォンのボタンを押す
→解錠ボタンを押す
→インターフォンのボタンを押す
のAPIを順番に叩くと、インターフォンのボタンを押す
→インターフォンのボタンを押す
→解錠ボタンを押す
のように処理が前後する可能性があります。このバグ?にハマって時間を無駄に使ってしまいました。
一時的な解決策として今回は、インターフォンのボタンを押す
→解錠ボタンを押す
の後に、数秒遅延させてインターフォンのボタンを押す
を最後に実行するようにしました。最後に、
URLの内容を取得
のアクションを追加し、インターフォンのボタンを押す
用のWebHookのURLを指定します。これは、インターフォンの終話を目的としています。※
インターフォーンのボタンを押す
箇所のボタン部分はなかなかSwitchBotと相性が悪く、SwitchBotが完全にボタンを押し込めない状態になることがありました。対策として、クッションを間に挟むことで押し込む動作を補強するようにしました。動作
随分わかりにくいですが、SwitchBot Hubにリクエストがいくと、SwithBot Hubが点滅し、SwitchBotが動作します。一瞬、赤いランプが光る箇所は、解錠ボタンが押された時になります。
※このgif画像では、3番目のAPIリクエストの動作部分がなぜか切れて写っておりません。
応用
今回のレシピでは、エントランスの自動解錠にSwitchBotを活用しましたが、インターネット経由(あるいは、ローカルネットワーク)で物理スイッチを押したいケースにいろいろと活用できるのではないかと思います。
SSH接続でサーバに処理をさせる
概要
ショートカット経由で、SSH接続しサーバーに対して任意のコマンドを実行します。
ここでは、サーバ上で単純にdateコマンドを実行し、サーバー日時を表示します。準備
- ショートカットアプリ
- SSH接続用のサーバー
手順
新規ショートカット作成画面を開き、スクリプティングから
SSH経由でスクリプトを実行
を選択します。接続先の情報を設定します。ここで注目したいのは、iOS12では SSH接続でパスワード認証する必要があったのですが、iOS13.1から公開鍵認証ができるようになっています。
ここでは、公開鍵認証を使用し、認証項目でSSHキー
を選択します。
公開鍵をサーバー上に追加するには、SSHキー
項目を選択すると、画面が開くので、この画面で新しいキーを生成するとともに、公開鍵を共有
から任意の場所に公開鍵を配布することができるようになります。公開鍵をサーバーに追加する方法については省略します。次にサーバの実行結果を表示するようにします。実行結果を表示するには、
クイックルック
や通知を表示
などいろいろ方法がありますが、ここでは通知を表示
アクションを追加します。これで、今回のレシピは完了になります。
動作
応用
SSH接続して任意のコマンドを実行できるので、いろんなことができそうですね。
私が試してみたのは、サーバ上にヘッドレスChromeのpuppeteerをインストールして、自分が住んでいる地域の天気をWebページのDOMをスクリーンショットに保存して画像を表示させたり、同様にGithubの草画像のスクリーンショットを保存して画像を表示するようなことをしました。その他、外出先から簡単にサーバーの起動・再起動・停止をショートカットのメニュー選択経由で実行してみるのも利便性が高い様な気もします。
今後、ラズベリーパイにSSH接続して任意の機器を操作するようなことも試してみたいと思っています。
情報共有に活用する。
概要
ショートカットは情報共有の際にも活用しています。
気になった記事をTwitterやSlackに投稿したいこともありますよね?
私はTwitterを投稿すると同時に、Slackのチャンネルも同時に共有したいなーと思った時にこのレシピのショートカットを使用しています。ここでは、記事の共有シートからTwitterとSlackに投稿する方法について紹介します。
準備
- ショートカットアプリ
- Twitterアプリ
- Slackアプリ
- SlackのチームIDと投稿先のチャンネルID
- ショートカットの作成
手順
まずは、記事から共有するための記事のタイトルとリンクを取得します。
作成するショートカットでは、共有する記事のリンクを受け入れる必要があるため、ショートカットアプリのそのための準備として、ギャラリータブにある共有のためのショートカットを選択し、
Twitterでリンクを検索
をショートカットに追加します。このショートカットをベースに変更を加えていきます。
また、ショートカットの名称を任意に変更します。ここでは、twislakに変更します。
まずは、受け入れるの項目を
URL
だけ選択し、それ以外の項目は一旦全て削除します。次に
変数を設定
のアクションを追加し、変数をURLにします。URLのタイトルを取得するために、
名前を取得
アクションを追加します。次に、
テキスト
アクションを追加し、取得したタイトルをもとに、共有するテキストを作成します。
ここでは、"タイトル" URL
の形式でテキストを作成しています。※ 実際に上記の共有するテキストが作成できているかを確認するために、一度、
クイックルック
アクションを追加して、動作確認してみると良いかと思います。これで、共有するテキストの準備が整ったので、まずはTwitterの投稿のアクションを追加します。
これで、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_ID
とCHANNEL_ID
を調べて、構築したURLスキームのURLをショートカットアプリで開けば、Slackのアプリが起動し、指定のチャンネルへの自動遷移ができるということです。ただし、自分のワークスペースのSlackの
TEAM_ID
とCHANNEL_ID
はすぐに分かる気がしますが、外部のワークスペースのTEAM_ID
とCHANNEL_ID
を調べる方法はあるのでしょうか??もしかするとすぐに分かる方法があるかもしれませんが、私はCharles ProxyのiOSアプリでパケットキャプチャすることで調べることができました。詳細については、割愛しますが、Slackアプリを開いて、指定のチャンネルを開いた時のリクエストURLなどを参照することで特定できるかと思います。
Slackの投稿設定でずいぶんと長くなりましたが、ようやくSlackの投稿ができるようになりました。
その前に、Slackで手動投稿するためのテキストをクリップボードにコピーしておきます。
そこで、クリップボードにコピー
アクションを追加します。ここから、Slackアプリの指定のチャンネルに移動する設定になります。
URLスキーム設定用の
URL
アクションを追加し、slack://channel?team={TEAM_ID}&id={CHANNEL_ID}
の形式の文字列を追加します。最後に、
URLを開く
アクションを追加して完了になります。動作
作成した
twislak
のショートカットを使用して、気になる記事のTwitter投稿とSlack投稿を行なったgif画像になります。最後に手動でペーストするのは若干手間ですが、他の方法も模索していきたいと思います。応用
今回のレシピでは、URLスキームの起動を活用しました。他のアプリにあるURLスキームやSiriショートカットを調べることで、便利なショートカットが実現できるかもしれません。
さいごに
ここで紹介したレシピはほんの一部でまだまだ活用方法はたくさんあると思います。
- Pythonの実行環境のPythonistaアプリと連携する
- 任意のAPIを叩く
- 任意のWebページでJavaScriptを実行する
- SSH接続で外部サーバーと連携する
など、いろんなことができるかと思います。これからも、活用方法を見つけていたきたいと思います。
それでは、自分なりのショートカットでいろいろ自動化して楽しみましょう!enjoy!!
- 投稿日:2019-12-22T00:00:19+09:00
ARでマンガを覗き読み【iOS】
はじめに
AR、いいですよね。素敵な世界。
マンガ、いいですよね。素敵な世界。ということで、今回はiOSのARKitを利用して、紙のマンガを覗き読みできるアプリ
を実装してみました。
デモに利用させていただいているマンガは、
マンガボックスで連載中の「ネコの手、借りてます。」?
作家の遥那もより様から許可をいただき動画掲載させていただいてます???
最高オブ最高の癒しマンガなのでみなさん是非読んでください???いつでも、どこでも、だれもが、この素敵な世界を実現できるよう、
実装を、コメント多めで紹介します✨前準備
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 ]
を追加。
フォルダの中にマーカーとして検出したい画像(今回はマンガの表紙)を入れます。
この時、画像の名前と現実世界での大きさを入力します。
マーカーにふさわしい画像についてはこちらを参考にしてください。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から追加されたピープルオクルージョンの設定で、設定するとこのように人の指が原稿の手前にくるので、マンガの中を覗き読んでいる!という世界をよりリアルに実現できます。
ただ、まだ少し精度が甘くチリチリすることは否めないので、お好みでオフにして下さい?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) }完成?
これで、本屋さんでマンガに封がしてあっても、中身を覗き読みすることができちゃいますね?
サンプルアプリはこちらに公開しています✨
koooootake/MangaTrialAR-ios動画はこちら
もうひと作品お借りしたのは個人利用可能で作品を公開してくださっている「ハナカク」様?
とても絵が綺麗いいい本当にありがとうございますすす✨おわりに
やりたいこと
ここから、マンガのコマをVisionで分解してアニメーションさせたり
指先のジェスチャーを検知してページをめくったりして、現実を拡張したいいいAppleのARグラスが売り出され?
コンテンツの表現が紙やディスプレイに留まらず拡張される素敵な世界が楽しみです?おまけ
DeNAアドベントカレンダー終盤を担当したので
この記事を読んだ、ものづくり好きな人にオススメの記事を勝手に紹介?【3日で実装・公開】エモいアートな画像生成アプリ開発
エモい!!!じんむのアイコンもエモくなりました【Electron+GCP+Slack App】Slackのコメントをニコニコ動画風にプレゼンで流す方法
「Slack」のコメントというところが、社会人には超超超実用的✨退職者を送る技術 - Twilio と Socket.IO で作る電話マルチプレイシステムの小ネタ
電話を掛けて、キーパットを利用して、みんなで操作するゲームの作り方?発想良すぎぎぎ来年もべしべしものづくり楽しもううう?