- 投稿日: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-22T23:35:32+09:00
実務経験4ヶ月のインターン生がGASを用いて勤務報告を自動化してみた
初めに
閲覧していただき、ありがとうございます!
現在からくり株式会社でiOSエンジニアとしてインターンしております。
今回勤務報告を自動化してみたので、そちらを記事にしたいと思います?背景
インターン生(アルバイト)は月に一回勤務報告をSlackでしないといけないのですが、
いつも手入力で報告しており、これが滅茶苦茶めんどくさい、、、
「月1の作業くらい頑張れよ?」という野次が飛んできそうですが、
なんせエンジニアなので楽したい性分なのです?そもそもGASとは??
Google Apps Script(GAS)とは、Googleが提供するサーバーサイド・スクリプト環境のことです。
スプレッドシートやGoogleフォームなどのサービスを、
JavaScriptをベースとしたプログラム言語を使って操作することができます。実装方針
インターン生は上記写真のように出勤日時をGoogleカレンダーに記入しなくてはいけないので、
これらイベント情報を抽出して出勤時間を計算していきたいと思います。1.カレンダーから出勤のイベントを抽出
const myCalendar = CalendarApp.getCalendarById('自分のカレンダーID'); const attendanceDays = myCalendar.getEvents(getFirstDate(new Date()), getLastDate(new Date()), {search: "出勤"});//月の初めを取得 function getFirstDate (date) { return new Date(date.getFullYear(), date.getMonth(), 1); } //月の最後を取得 function getLastDate (date) { return new Date(date.getFullYear(), date.getMonth() + 1, 0); }
getCalendarById
メソッドを用いて自分のGoogleカレンダーを取得後、
getEvents
メソッドを用いて出勤のイベントだけを抽出します。2.勤務時間を計算
for(var i = 0; i < attendanceDays.length; i++) { var attendanceTime = attendanceDays[i].getStartTime() var leavingTime = attendanceDays[i].getEndTime() var actualWorkingHours = getActualWorkingHours(leavingTime.getHours(), attendanceTime.getHours()) }//実働時間を取得 function getActualWorkingHours(leavingTime, attendanceTime) { actualWorkingHours = leavingTime - attendanceTime if (actualWorkingHours == 9) { return actualWorkingHours - 1 } else { return actualWorkingHours } } //休憩時間を取得 function getBreakTime(workingHours) { if (workingHours == 9) { return 1 } else { return 0 } }取得してきた各々のイベントの終了時刻
getEndTime()
と開始時刻getStartTime()
を取得し、
その差分を計算します。
また、フル勤務の際(10:00~19:00)は1時間休憩をとる必要があり、
実働時間を正確に計算する為、差分から1時間引きます。3.Slackに投稿
function postMessage() { var url = "https://slack.com/api/chat.postMessage"; var payload = { "token" : "アクセストークン", "channel" : "投稿したいチャンネルのID", "text" : "<@メンションしたい相手のID>投稿するメッセージ, "as_user" : true }; var params = { "method" : "post", "payload" : payload }; UrlFetchApp.fetch(url, params); }SlackAPIの
chat.postMessage
に関してはコチラの記事がわかりやすいです。4.Google Apps Scriptにトリガーを追加する
最後に月の初めに自動で実行されるようにGoogle Apps Scriptにトリガーを追加します。
プロジェクトのトリガーボタンからトリガーの設定をします。
トリガーの一覧画面右下のトリガーを追加ボタンを選択します。
イベントのソースを選択で定期的に実行したい時間を適宜設定します。
今回は月の初めに実行してほしいので、下記のように設定しました。これで月の初めに自動で実行されるようになりました!
成果物
これで毎月の勤務報告を手入力をしなくても
自動で勤務報告ができるようになりました!(送り忘れることもなし)まとめ
普段Swiftを書いていて、今回初めてJavaScript(GAS)を触ったのですが、
なんとか形にすることができてとりあえずホッとしてます笑(なんせこの土日で勉強して書いた為、、、ギリギリ?)
まだ交通費の記入や15分単位での計算に対応できてないので、
その辺りを改善していきたいと思います。
また、コードを書いている内にあれも自動化したいな〜?
みたいなのが色々出てきたので、社員の方がより重要な業務に時間を割けるように
無駄な作業はインターン生で自動化していければなと思います!
(社員さんは忙しくてなかなか手が回らないので、、、)最後に
僕がインターンをしているからくり株式会社では現在エンジニアを募集しているそうです!
(インターン、新卒、中途問わず)
興味がありましたら、是非一度遊びに来てくださいね〜!!
Wantedly
- 投稿日: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-22T19:44:53+09:00
メモ
//呼び出し元 @objc enum Status: Int { case web case normal case none func get1(str: String) -> Status { switch str { case "WEB会員": return .web case "ノーマル会員「": return .normal default: return .none } } } class Enum { func get() { // 文字列取得 let sta = "WEB会員" // 初期値取得 let status: Status = .none // 文字列をもとに数字を取得今回は0 let i = status.get1(str: sta).rawValue // dictに詰める var dict: [String: Any] dict = ["rank": i] let a = Tamu() a.consoleWrite(dict) } }呼び出し先(objective-c側)
- (void)consoleWrite:(NSDictionary *)dict { // integerValueで 型変換を行う。でないと正式にint型になってないから NSInteger ka = [dict[@"rank"] integerValue]; if(StatusWeb == ka) { NSLog(@"a"); } }
- 投稿日: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:14:58+09:00
RxSwift + MVVM の学習をしよう(サンプルアプリ付き)
この記事は フラーAdventCalendar2019 の22日目の記事です。
前回の21日目は@sacchoさんで AndroidでKotlin Coroutinesを使ってソケット通信をしてみる でした。私は現在、フラー株式会社でiOSエンジニアとしてアルバイトをさせて頂いています。
今年を振り返るとRxSwift,MVVMをある程度まで習得するのが一番大変だったなと感じています。(現在もですが、、笑)
なので今回は「RxSwift,MVVMを学習するのに参考になった記事を紹介する」 + 「RxSwift,MVVMを使って何か簡単なアプリを作成する」の2点について書いていきたいと思います。
そして、RxSwift初学者の人の参考になれば嬉しいです。1.RxSwift,MVVMを学習するのに参考になった記事
RxSwift
用途別に紹介していきます。
まず、RxSwiftを最近触り始めたけど、よく理解できていない方にオススメです。また、2019年のiOSDCの自作して理解するリアクティブプログラミングフレームワークはとても参考になりました。(URLは動画となっています。)
次はオペレーターを理解する上で参考にさせて頂いた記事です。
最後は、実践的な使用方法を学習する上で参考になった記事です。
- RxSwiftでの実装練習の記録ノート(前編:Observerパターンの例とUITableViewの例)
- RxSwiftでの実装練習の記録ノート(後編:DriverパターンとAPIへの通信を伴うMVVM構成のサンプル例)
- RxSwiftのすぐに取り込める使用例をまとめてみた
MVVM
自分はまず、アーキテクチャって言葉自体聞いた事ない状態からだったので以下の記事は分かりやすく、ありがたかったです。
また、最近はiOSアプリ設計パターン入門 を購入して勉強しています。
2.RxSwift + MVVMで簡単なStopWatchアプリを作成する
概要
下記の画面の画像にあるように、Startボタンを押すと0.1秒ごとにカウントされ、Stopボタンが出ます。そして、Stopボタンを押すとカウントが止まってStartボタンとResetボタンが出てきます。
このように普通のStopWatchアプリです。
タイトルの方にもMVVMって書いてあるんですが、今回のアプリではModel
は使う機会がありませんでした。なので、ViewController
とViewModel
のみの実装となります。
また、StoryBoardを使わずにCodeのみで作成しました。GithubRepository
https://github.com/SUGIYOSI/StopWatchSample
画面の画像
使用ライブラリ
- RxSwift
- RxCocoa
- SnapKit(今回はあまり重要ではない)
環境
- Xcode 11.2.1
- Swift 5.1.2
ViewController
今回使ったUI部品は以下の3つです。(上の画像を参考にしてください)
-timerLabel (UILabel)
-startStopButton (UIButton)
-resetButton (UIButton)
RxSwiftの処理を追うのは初めのうちは結構大変なので、今回はViewとViewModelの動きを以下のように細かく書きました。正確じゃないところもありますが、参考程度にはなると思います。ViewModelとViewControllerを照らし合わせて見てください。あと字が汚くてすみませんw
下記のコードはViewを作成しているコードを抜かしているのでコピペしていじってみたい方は、Githubの方からしてください。
StopWatchViewController.swiftclass StopWatchViewController: UIViewController { private var viewModel: StopWatchViewModelType private let disposeBag = DisposeBag() init(viewModel: StopWatchViewModelType) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupSubView() bind(viewModel) viewModel.inputs.isPauseTimer.accept(false) } } extension StopWatchViewController { func bind(_ viewModel: StopWatchViewModelType) { startStopButton.rx.tap.asSignal() .withLatestFrom(viewModel.outputs.isTimerWorked) .emit(onNext: { [weak self] isTimerStop in self?.viewModel.inputs.isPauseTimer.accept(!isTimerStop) }) .disposed(by: disposeBag) resetButton.rx.tap.asSignal() .emit(to: viewModel.inputs.isResetButtonTaped) .disposed(by: disposeBag) viewModel.outputs.isTimerWorked .drive(onNext: { [weak self] isWorked in if isWorked { self?.startStopButton.backgroundColor = UIColor(red: 255/255, green: 110/255, blue: 134/255, alpha: 1) self?.startStopButton.setTitle("Stop", for: UIControl.State.normal) } else { self?.startStopButton.backgroundColor = UIColor(red: 173/255, green: 247/255, blue: 181/255, alpha: 1) self?.startStopButton.setTitle("Start", for: UIControl.State.normal) } }) .disposed(by: disposeBag) viewModel.outputs.timerText .drive(timerLabel.rx.text) .disposed(by: disposeBag) viewModel.outputs.isResetButtonHidden .drive(resetButton.rx.isHidden) .disposed(by: disposeBag) } }ViewModel
ViewModel
はInput
とOutput
を分ける事で可読性をあげたり、バグを未然に防ぐ事ができます。
また、Input
、Output
は必ずDriver
、Signal
を使用しています。この
ViewModel
を作成するにあたって、以下の3つの記事を参考にしました。
- RxExample MVVM のその先へ(Fat ViewModel の倒し方)
- friends.nico iOSのリリースと実装について
- RxSwiftへ苦手意識がある方向けの RxSwift + MVVM でiOSサンプルコード書きました
- RxCocoa 4 の Signal と Relay のまとめ
また、
Observable<Int>.interval(0.1, scheduler: MainScheduler.instance)
を使用する際にこの記事と同じように躓いてしまったので参考にさせてもらいました。StopWatchViewModel.swiftprotocol StopWatchViewModelInputs: AnyObject { var isPauseTimer: PublishRelay<Bool> { get } var isResetButtonTaped: PublishRelay<Void> { get } } protocol StopWatchViewModelOutputs: AnyObject { var isTimerWorked: Driver<Bool> { get } var timerText: Driver<String> { get } var isResetButtonHidden: Driver<Bool> { get } } protocol StopWatchViewModelType: AnyObject { var inputs: StopWatchViewModelInputs { get } var outputs: StopWatchViewModelOutputs { get } } final class StopWatchViewModel: StopWatchViewModelType, StopWatchViewModelInputs, StopWatchViewModelOutputs { var inputs: StopWatchViewModelInputs { return self } var outputs: StopWatchViewModelOutputs { return self } // MARK: - Input let isPauseTimer = PublishRelay<Bool>() var isResetButtonTaped = PublishRelay<Void>() // MARK: - Output let isTimerWorked: Driver<Bool> let timerText: Driver<String> let isResetButtonHidden: Driver<Bool> private let disposeBag = DisposeBag() private let totalTimeDuration = BehaviorRelay<Int>(value: 0) init() { isTimerWorked = isPauseTimer.asDriver(onErrorDriveWith: .empty()) timerText = totalTimeDuration .map { String("\(Double($0) / 10)") } .asDriver(onErrorDriveWith: .empty()) isResetButtonHidden = Observable.merge(isTimerWorked.asObservable(), isResetButtonTaped.map { _ in true }.asObservable()) .skip(1) .asDriver(onErrorDriveWith: .empty()) isTimerWorked.asObservable() .flatMapLatest { [weak self] isWorked -> Observable<Int> in if isWorked { return Observable<Int>.interval(0.1, scheduler: MainScheduler.instance) .withLatestFrom(Observable<Int>.just(self?.totalTimeDuration.value ?? 0)) { ($0 + $1) } } else { return Observable<Int>.just(self?.totalTimeDuration.value ?? 0) } } .bind(to: totalTimeDuration) .disposed(by: disposeBag) isResetButtonTaped.map { _ in 0 } .bind(to: totalTimeDuration) .disposed(by: disposeBag) } }最後に
StopWatchアプリの方で改善した方がいい箇所があればコメントしていただければ嬉しいです。
これからも自分のペースで楽しく学習していければなと思います。
やっぱりRxSwiftは難しいなと思いました。
- 投稿日: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: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-22T14:06:06+09:00
extensionの条件付き適合の謎
ことの発端
条件付き適合を使用して処理を分けつつ共通化していたとき
なぜか条件付き適合ではない方の処理を実行していました。環境
swift 5
Xcode 11.3ソースコード
protocol Hoge { associatedtype Value func value(_ value: Value?) -> Value? } struct HogeImple<Value> { // jQuery風に value が届いたら保存、なければ読込 func value(_ value: Value? = nil) -> Value? { if let value = value { self.store(value) return value } return self.read(value) } } private extension Hoge { func store(_ value: Value?) { // 保存 /* !!! String, Codable もこちらにくる!? !!! */ } func read() -> Value? { // 読込 } } private extension Hoge where Value == String { func read(_ value: Value?) { // 文字列の読込 } } private extension Hoge where Value: Codable { func store(_ value: Value?) { // Codableの保存 } func read() -> Value? { // Codableの読込 } }実験
条件付き適合が動作する条件は何か実験してみました。
protocol Hoge { associatedtype Value } struct HogeImple<Value>: Hoge { func execute(_ value: Value) { self.hoge(value) } } extension Hoge { func hoge(_ value: Value) { print("Hoge", value) } } extension Hoge where Value == String { func hoge(_ value: Value) { print("String", value) } } extension Hoge where Value: Numeric { func hoge(_ value: Value) { print("Numeric", value) } } let boolHoge = HogeImple<Bool>() boolHoge.execute(true) boolHoge.hoge(true) let strHoge = HogeImple<String>() strHoge.execute("Value") strHoge.hoge("Value") let intHoge = HogeImple<Int>() intHoge.execute(3) intHoge.hoge(3)結果
Bool型 : 適合条件なし
String型 :where Value == String
に適合
Int型 :where Value: Numeric
に適合execute
struct HogeImple
内の関数
hoge
extention Hoge
内の関数
型 関数 結果 Bool execute Hoge true Bool hoge Hoge true String execute Hoge Value String hoge String Value Int execute Hoge 3 Int hoge Numeric 3 まとめ
- 具象型からは extension の適合条件で分岐することができない
これはバグだと思うんですが、どうなんでしょうか?
- 投稿日: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-22T09:53:38+09:00
AWS Amplifyを使ってCognito User PoolsログインするSwiftUIのサンプル
はじめに
最近Amplifyを利用し始めたのですが、Amplify CLIでちょっとコマンドを実行するだけで、アプリからAWSのリソースを利用できるようになり、すごく便利に感じています。
ここでは、Amplifyを使って、Cognito User Poolsログインする処理をSwiftUIで行う方法を説明します。
ドキュメントには、ログインやログアウトなど一つ一つの例はあるのですが、具体的にアプリにどのように組み込むかまでは記載されていません。そこで、このサンプルが具体的にアプリにどう組み込むか悩んでいる方に役に立つのではないかと思い、書きました。
前提
バージョンは次の通りです。
- Xcode 11.2.1
- Amplify iOS SDK 2.0
また、この記事では、サインインの処理を具体的にどう組み込むかのみを説明します。
CocoaPodsに依存関係を設定して、
$ amplify add auth
して、...のような基本的な手順は割愛します。基本的な利用手順については、以下のドキュメントをざっと読んでいただければと思います。
https://aws-amplify.github.io/docs/sdk/ios/authenticationイメージ
アプリを起動して、ユーザーがログイン済で出ない場合、ログイン画面を開きます。
ログインしたら、ホーム画面に遷移します。Podfile
target 'MyApp' do use_frameworks! pod 'AWSMobileClient', '~> 2.12.0' target 'MyAppTests' do inherit! :search_paths end target 'MyAppUITests' do end endSceneDelegate
ポイントは以下の2つです。
AWSMobileClient.initialize(completionHandler:)
でAWSMoblieClientを初期化し、結果に応じてホーム画面またはログイン画面を開くAWSMobileClient.addStateListener(object:callback:)
でuserStateをobserveし、userStateの変化に応じてホーム画面またはログイン画面を開くSceneDelegate.swiftimport UIKit import SwiftUI import AWSAppSync import AWSMobileClient class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) AWSMobileClient.default().initialize { [weak self, window] (userState, error) in guard let self = self else { return } if let error = error { print("### AWSMobileClient.initialize.error", error.localizedDescription) return } if let userState = userState { switch (userState) { case .signedIn: self.showHomeView(in: window) // ホームを開く case .signedOut: self.showSignInView(in: window) // ログインページを開く default: AWSMobileClient.default().signOut() self.showSignInView(in: window) // ログインページを開く } } } AWSMobileClient.default().addUserStateListener(self) { [weak self, window] (userState, info) in guard let self = self else { return } switch (userState) { case .signedIn: self.showHomeView(in: window) // ホームを開く case .signedOut, .signedOutUserPoolsTokenInvalid: self.showSignInView(in: window) // ログインページを開く default: AWSMobileClient.default().signOut() self.showSignInView(in: window) // ログインページを開く } } } } private func showHomeView(in window: UIWindow) { DispatchQueue.main.async { window.rootViewController = UIHostingController(rootView: HomeView()) self.window = window window.makeKeyAndVisible() } } private func showSignInView(in window: UIWindow) { DispatchQueue.main.async { window.rootViewController = UIHostingController(rootView: SignInView(viewModel: SignInViewModel())) self.window = window window.makeKeyAndVisible() } } }SignInView
パスワードの初期化とサインアップは割愛させていただきます。
SignInView.swiftimport SwiftUI struct SignInView: View { @ObservedObject var viewModel: SignInViewModel var body: some View { VStack(spacing: 20) { Spacer() Text("MyApp") .bold() .font(.title) Spacer() TextField("Username", text: $viewModel.userName) .autocapitalization(.none) .padding() .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color.gray, lineWidth: 1) ) SecureField("Password", text: $viewModel.password) .padding() .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color.gray, lineWidth: 1) ) Button(action: { print("### signinButton did tap") self.viewModel.signIn() }) { HStack { Spacer() Text("Sign in") Spacer() } .padding() } .accentColor(.white) .background(Color.green) .cornerRadius(6) .alert(isPresented: $viewModel.showAlert) { Alert(title: Text("Sign in error"), message: Text(viewModel.errorMessage)) } HStack { Spacer() Button(action: { fatalError("forget password hasn't be implemented.") // 省略 }) { Text("Forget password?") } } Spacer() HStack { Spacer() Text("Don't have an account?") Button(action: { fatalError("sign up hasn't be implemented.") // 省略 }) { Text("Sign up") } Spacer() } Spacer() } .padding(EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15)) } } struct SignInView_Previews: PreviewProvider { static var previews: some View { SignInView(viewModel: SignInViewModel()) } }SignInViewModel
SignInViewModel.swiftimport Foundation import AWSMobileClient class SignInViewModel: ObservableObject { @Published var userName: String = "" @Published var password: String = "" @Published var showAlert = false @Published var errorMessage: String = "" func signIn() { AWSMobileClient.default().signIn(username: userName, password: password) { (result, error) in if let error = error { print("### signin error:", error.localizedDescription) self.showAlert = true self.errorMessage = error.localizedDescription } print("### Signin success") } } }HomeView
HomeView.swiftimport SwiftUI struct HomeView: View { var body: some View { Text("Home") .font(.title) } } struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() } }以上です。
- 投稿日:2019-12-22T06:30:54+09:00
GithubActionsでプライベートリポジトリをsubmoduleとして取り込む
GithubActionsでプライベートリポジトリをsubmoduleとして取り込む方法はsubmodule addコマンドでhttps方式で追加したかssh方式で追加したかによって変わります。それぞれのメリットデメリットを説明しつつ、実現方法を記しておきます。
https方式 git submodule add https://github.com/u-nation/GITHUB_REPOSITORY.git ssh方式 git submodule add git@github.com:u-nation/GITHUB_REPOSITORY.githttps方式
手軽なのはhttps方式です。
以下の権限を持つパーソナルアクセストークンを作成し、secretsに登録してワークフロー内でwithオプションに渡してあげるだけで済みます。
※パーソナルアクセストークンはhttps方式しか使えません。1
.github/workflows/sample.yml- name: submodule uses: actions/checkout@v1 with: submodules: true token: ${{ secrets.PERSONAL_TOKEN }}
- メリット 手軽さ
- デメリット パーソナルアクセストークンは発行したアカウントがアクセスできるリポジトリ全てに権限が及ぶのでトークンが漏れた場合のセキリュティリスクが大きい。Bot用のアカウントを作成やトークン作成の依頼・共有など運用がしんどい。
ssh方式
- ssh-keygenコマンドで秘密鍵公開鍵を作成
- submodule先のリポジトリのDeploy Keysに公開鍵を登録(必要に応じてwrite権限を付与)
- 取り込む方のリポジトリのsecretsに秘密鍵を登録
- ワークフローでsshの設定とsubmodule update --initをする
.github/workflows/sample.yml- uses: actions/checkout@v2 - name: SSH Setting env: TOKEN: ${{ secrets.TOKEN }} run: | mkdir -p /home/runner/.ssh/ echo -e "$TOKEN" > /home/runner/.ssh/id_rsa chmod 600 /home/runner/.ssh/id_rsa # https://github.com/actions/checkout#checkout-submodules - name: Checkout submodules shell: bash run: | git submodule sync --recursive git submodule update --init --force --recursive
- メリット 鍵の権限はそのリポジトリに限定されるので漏れた時のセキュリティリスクが軽減される。必要に応じて各々が鍵の設定を追加できるので運用が楽。
- デメリット 鍵作ったり設定するのがちょっとだけめんどい。
所感
企業でGithubを使う場合は大抵プライベートなリポジトリだと思うので、誰かのお役に立てると嬉しいです。
ssh方式を実現するのに6時間位ハマって辛かったですが、運用が楽になるのでssh方式がオススメです。
注釈
注釈
GithubActionsに移行する前は、AWSのCodeBuildを使用していました(GithubAcitonsの方がめちゃくちゃ早いです)。その時は秘密鍵をSystem Managerで用意してssh方式をしていましたが、途中でhttps方式のsubmoduleオプションが登場しました。 ↩
- 投稿日: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-22T03:48:18+09:00
iOS13で新しく追加されたMetricKitがとてもすごい件
この記事は、ZOZOテクノロジーズ #5AdventCalendar2019の記事です。
昨日は@EnKUMAさんの「GKE上でgcloudコマンドを叩く為のPodをDeployしたら詰まった話」の記事でした。
ZOZOテクノロジーズ iOSエンジニアの@ahiruです。
本稿ではiOS13 Xcode11より新しく追加されたフレームワークの一つであるMetricKitについて調べたことなどをまとめていきます。
WWDC 2019の「Improving Battery Life and Performance」にてMetricKitの詳細が説明されておりますので、このセッションをベースにMetricKitとは何か、どんなことができるのかなどを中心に書いていきます。
What's MetricKit?
Appleのドキュメントを見てみると以下のように記述されています。
Aggregate and analyze per-device reports on power and performance metrics
MetricKitはデバイス上のアプリの電力やパフォーマンスなどのメトリックを受け取るためのフレームワークです。
MetricKitを使うことでバッテリーとパフォーマンスへの影響を定量化できるようになります。
すなわち、アプリのパフォーマンスを向上させる上でどの機能がボトルネックになっているのか、何が原因で起動時間が遅いのかなどの原因の特定をかなり簡単に調べることができるようになりました。
メトリックスの種類
Battery MetricsとPerformance Metricsの2のメトリックが存在します。
Battery Metrics
- Processing
- Location
- Display
- Networking
- Accessories(Bluetooth)
- Multi Media
- Camera
Performance Metrics
- Hangs
- Disk
- Application Launch
- Memory
- Custom Intervals
メトリックスを取得するための3つのツール
iOS13 Xcode11よりメトリックスを収集するための3つの機能が追加されました。
下記で紹介する3つの機能をうまく使うことで各開発段階(開発・テスト / ベータ版 / リリース版)のそれぞれでメトリックスを収集することが可能となります。
1. XCTest Metrics
これまでもXCTestからメトリックスを取得することは可能でしたが、取得できるメトリックが増えました。
UIテスト・ユニットテスト両方で使用可能です。
measure(metrics: [XCTMetric], block: () -> Void)
- 【 iOS13 Xcode11未満 】では実行時間のみが計測可能だった
func testPhotoUploadPerformance() { let app = XCUIApplication() // 実行にかかる時間を測定 measure() { app.buttons["Apply Effect"].tap() app.dialogs["alert"].buttons["OK"].tap() } }
- 【 iOS13 Xcode11以降 】では取得したいメトリックを指定することが可能 (取得できるメトリックについてはこちらを参照)
func testPhotoUploadPerformance() { let app = XCUIApplication() // 実行にかかる時間 / メモリ使用量 / CPU使用率 を測定する measure(metrics: [XCTClockMetric(), XCTMemoryMetric(application: app), XCTCPUMetric(application: app)]) { app.buttons["Apply Effect"].tap() app.dialogs["alert"].buttons["OK"].tap() } }testApplicationLaunchTime
また、新しいUIテストの各ターゲットにはアプリケーションの起動テストが自動でテストを追加されるようになりました。
func testApplicationLaunchTime() { measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { XCUIApplication().launch() } }実際にこのテストを実行してみた方はわかると思うのですが、5回くらいアプリが立ち上がります。
その後テストの実行ボタンの下に詳細ボタンが表示されるのでそれをクリックすると下のようなポップアップが表示されます。
5回の起動の平均時間がAverageとして表示され、それをBaselineに設定することができます。いわば、そのアプリの起動時間の基準値です。
自身が開発中のアプリに新しい機能の追加などを行った際に、起動時間が基準値よりも遅くなった場合にはその機能のパフォーマンスが適切とは言えません。
パフォーマンスの側面からリグレッションテストが可能となりました。パフォーマンステストを行う際の注意点として以下の点がセッションの中で挙げられていました。
- 負荷がかかるのでデバッガーは加えない
- 全ての診断オプションは切っておく
- 独立したスキームを使うかテストプラン機能を使うことで診断オプションは簡単にオフにできる
2. MetricKit
メトリックを収集するためのオンデバイスフレームワークです。
各ユーザによるアプリケーション使用時の状況を理解するのに役立ちます。
主にベータ版・リリース版に対して詳細なメトリックスを収集するために使用します。
実装方法
実装方法はとても簡単です。
クラスを作成しメトリックスの受信を許可するだけです。するとデータ(メトリックス)の収集が開始されます。
class MySubscriber: NSObject, MXMetricManagerSubscriber { var metricManager: MXMetricManager? override init() { super.init() metricManager = MXMetricManager.shared // メトリックスの受信を許可する metricManager?.add(self) } deinit { metricManager?.remove(self) } func didReceive(_ payloads: [MXMetricPayload]) { for metricPayload in payloads { // 各自、ペイロードをサーバーに送ったり、ファイルに保存したりする } } }MetricKitを使うことでアプリがいつどこでどれくらい使われたのかを簡単に知ることができます。
24時間が経過するとそのサマリが作成されてデバイスに返されます。上記の実装をすると1日1回自動でメトリックスの収集が行われます。mxSignPost
アプリ内のある機能やあるアクションに関するメトリックスを収集することができます。
実装方法はとても簡単で対象の箇所をmxsignPostで囲むだけです。
let photosLogHandle: OSLog = MXMetricManager.makeLogHandle(category: "Photos") mxSignpost(.begin, log: photosLogHandle, name: "SavePhoto") // パフォーマンス計測したい処理をここに記述 mxSignpost(.end, log: photosLogHandle, name: "SavePhoto")上記のように実装すると、MetricKitが自動でメトリックスを収集してくれます。
3. Xcode Metrics Organizer
コードを変更することなくOrganizerから集約されたデータの確認ができます。
主にリリース版で生じた問題を収集するために使用します。
Window -> Organizer -> Metricsのタブより確認ができます。
個人開発のアプリで確認してみた結果がこちらです。(アップデートしたら起動時間が短縮されてて嬉しい...!!!)
起動時間やバッテリーの使用量などのデータを見ることができます。バージョン毎に確認できるので、テストの時はパフォーマンスに問題なかったのにリリース版ではパフォーマンスが悪くなっているなどの確認も簡単に行えます。
いろんなメトリックがグラフで可視化されていて面白いです。
皆さんの携わっているプロダクトでもぜひ見てみてください。
まとめ
WWDC 2019は内容が盛り沢山だったためMetrics周りに言及された国内の記事はあまり見かけませんが、実は結構な進化を遂げていました。
アプリのパフォーマンス向上はアプリ開発者なら誰しもが意識するところだと思います。
XCTestでいろんなメトリックからパフォーマンス計測を簡単に行えたり、MetricKitを使ってリリース版のメトリックスを簡単に取得できるのはハイクオリティのアプリケーションを作る上でとても大きな恩恵だと思います。
MetricKitを使って質の高いアプリを作っていきましょう!!
- 投稿日:2019-12-22T02:18:40+09:00
SwiftUIとSpriteKitでクリスマスツリー作る
こんにちは、
オークファンAdvent Calendar 24日担当のRickyです。19新卒として入社して半年強が経ちました。
先週人生初の日本忘年会を体験しました。そういえば明日クリスマスですね。
この前Advent Calendar 12日の記事を書き終わったら、
うちの同期に24日の記事でなんかクリスマスに関する記事を書いてくださいよと
煽られましたから今日はSwiftUIとSpriteKitでクリスマスツリーを作りましょう。ちなみに、
持っているMacBookAirのOSは10.14.6なのでSwiftUIのCanvasは使えない。。。。環境
流れ
1、雪背景SpriteKitParticleとイリュミネイションSpriteKitParticle作成
2、雪のUIView作成
3、クリスマスツリーUIView作成
4、ギフトUIView作成
5、文字UIView作成
6、SwiftUIのContentViewに纏める1、雪背景SpriteKitParticleライトイリュミネイションSpriteKitParticle作成
SnowParticle.sks作成
StarParticle.sks作成
値設定
Light1Particle.sks作成
値設定
Light2Particle.sks作成
値設定
2、雪のUIView作成
SnowBackgroundView.swiftimport SwiftUI import SpriteKit struct SnowBackgroundView: UIViewRepresentable { class Coordinator: NSObject { var scene: SKScene? } func makeCoordinator() -> Coordinator { return Coordinator() } func makeUIView(context: Context) -> SKView { //SkView作成 let skView = SKView() //FPSの表示 skView.showsFPS = true //ノード数表示 skView.showsNodeCount = true //シーン作成 let scene = SKScene() //spriteの中心を設定する scene.anchorPoint = CGPoint(x: 0.5, y: 1) //ノードを作成 let emitterNode = SKEmitterNode(fileNamed: "SnowParticle") //シーンにノード入れる scene.addChild(emitterNode!) scene.scaleMode = .resizeFill //シーンをこのviewの内容に設定 context.coordinator.scene = scene return skView } func updateUIView(_ view: SKView, context: Context) { view.presentScene(context.coordinator.scene) } } struct SnowBackgroundView_Previews: PreviewProvider { static var previews: some View { SnowBackgroundView() } }3、クリスマスツリーUIView作成
TreeView.swiftimport SwiftUI import SpriteKit struct TreeView: UIViewRepresentable { class Coordinator: NSObject {var scene: SKScene? } func makeCoordinator() -> Coordinator { return Coordinator() } func makeUIView(context: Context) -> SKView { //SkView作成 let skView = SKView() //シーン作成 let scene = SKScene() //クリスマスてツリーノードを作成 let treeNode = SKSpriteNode(imageNamed: "tree") treeNode.position = CGPoint(x: UIScreen.main.bounds.size.width/2, y: UIScreen.main.bounds.size.height/3) //スターノードを作成 let starNode = SKSpriteNode(fileNamed: "StarParticle") starNode?.position = CGPoint(x: 0, y: treeNode.position.y-50) //ライトノードを作成 let lightNode1 = SKSpriteNode(fileNamed: "Light1Particle") lightNode1?.position = CGPoint(x: 0, y: starNode!.position.y-120) let lightNode2 = SKSpriteNode(fileNamed: "Light2Particle") lightNode2?.position = CGPoint(x: 0, y: starNode!.position.y-200) //クリスマスツリーノードにスターノードとライトノードを入れる treeNode.addChild(starNode!) treeNode.addChild(lightNode1!) treeNode.addChild(lightNode2!) //シーンクリスマスツリーノードを入れる scene.addChild(treeNode) scene.scaleMode = .resizeFill //シーン背景色を透明で黒色にする scene.backgroundColor = UIColor.init(displayP3Red: 0, green: 0, blue: 0, alpha: 0.1) //シーンをこのviewの内容に設定 context.coordinator.scene = scene return skView } func updateUIView(_ view: SKView, context: Context) { view.presentScene(context.coordinator.scene) } } struct TreeView_Previews: PreviewProvider { static var previews: some View { TreeView() } }4、ギフトUIView作成
GiftView.swiftimport SwiftUI struct GiftView: View { var body: some View { ZStack{ //重ねるギフトの絵作成 Image("gift") .position(x: 130, y: 440) Image("gift1") .position(x: 150, y: 450) Image("gift2") .position(x: 220, y: 450) } } } struct GiftView_Previews: PreviewProvider { static var previews: some View { GiftView() } }5、文字UIView作成
TextView.swiftimport SwiftUI struct TextView: View { var body: some View { //文字作成 Text("Merry Christmas") //フォント設定 .font(.largeTitle) //フォント太さ設定 .fontWeight(.bold) .padding() //文字の色 .foregroundColor(Color.white) //枠の線 .background(Color.red) //文字と距離設定 .padding(10) //枠の線の太さと色設定 .border(Color.gray, width: 5) } } struct TextView_Previews: PreviewProvider { static var previews: some View { TextView() } }6、SwiftUIのContentViewにまとめる
ContrntView.swiftimport UIKit import SwiftUI import SpriteKit struct ContentView: View { //@Stateプロパティを使ってUIの状態自動に同期 @State private var displayTree = false var body: some View { //Z軸にビューを並べるコンポーネント ZStack{ //縦軸にビューを並べるコンポーネント VStack{ if(displayTree) { ZStack{ //クリスマスツリーとギフトviewを呼び出す TreeView() GiftView() } } Button(action: { //ON/OFFを制御するコンポーネント self.displayTree.toggle() }, label: { TextView() }) } //雪のviewを呼び出す SnowBackgroundView() //雪のviewを半透明にする .opacity(0.5) }.background(SwiftUI.Color.black.edgesIgnoringSafeArea(.all)) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }Merry Christmas
![]()
ソースコード
- 投稿日: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 で作る電話マルチプレイシステムの小ネタ
電話を掛けて、キーパットを利用して、みんなで操作するゲームの作り方?発想良すぎぎぎ来年もべしべしものづくり楽しもううう?