koogawa blog

iOS、Android、foursquareに関する話題

iOSで検知できるセンサー12項目をまとめた「iSensor」のSwift版を公開しました

輝度センサーやモーションセンサーなど、iOSで検知できる様々な項目をまとめたサンプル集 iSensorSwift Githubで公開しました。

f:id:koogawa:20160429213238p:plain

github.com

▼機能の1つである「輝度センサー」のスクリーン

f:id:koogawa:20131117122515p:plain

iSensorSwift は昔書いたこちらのサンプル集を Swift で書き直したものになります。

興味のある方は、ぜひ使ってみてください!😀

検知できる項目

  • 光・音声系
  • 位置情報系
  • 移動・動作系
    • 加速度センサー
    • 歩数・進行状況
    • 移動速度
  • その他
    • 顔検出
    • バッテリー残量

実装方法を解説した記事

【Tips】iOSの顔検出機能を使ってみる(Swift編)

数年前に書いた下記記事が古くなってきたので、Swift編 として書き直しました。

【Tips】iOSの顔検出機能を使ってみる - koogawa blog

f:id:koogawa:20160429092322p:plain


iOS 5から追加された CIDetector を使って、顔検出機能を使う方法をメモしておきます。

実装方法

まずは「CoreImage.framework」を追加します。

f:id:koogawa:20131122152458p:plain

インスタンスを生成します。

let detector = CIDetector(ofType: CIDetectorTypeFace,
                          context: nil,
                          options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])

ofType には検出種類を指定します。今回は顔検出なので、CIDetectorTypeFace を指定します。

options には検出精度 CIDetectorAccuracy 等を指定できます。

  • CIDetectorAccuracyLow - 精度は低いが、パフォーマンスは良い
  • CIDetectorAccuracyHigh - 精度は高いが、パフォーマンスは悪い

次に、顔検出を実行します。

// UIImage から CGImage を作る
let cgImage = image.CGImage
// CGImage から CIImage を作る
let ciImage = CIImage(CGImage: cgImage)
// 顔検出実行
let features = detector.featuresInImage(ciImage,
                           options: [CIDetectorSmile : true])

featuresInImage の引数には、顔検出したい画像を CIImage クラスで指定します。

今回は検出された顔が笑っているかどうかも検出したいので optionsCIDetectorSmile をセットしました。

正常に顔が検出されると、検出結果が CIFaceFeature オブジェクトとして返ってきます。このオブジェクトには、検出された顔の範囲や、目と口の位置などが含まれます。

  • bounds - 顔の範囲
  • hasLeftEyePosition - 左目の位置を検出できたか
  • hasMouthPosition - 口の位置を検出できたか
  • hasRightEyePosition - 右目の位置を検出できたか
  • leftEyePosition - 左目の位置
  • mouthPosition - 口の位置
  • rightEyePosition - 右目の位置
  • hasSmile - 顔が笑っているか
  • leftEyeClosed - 左目が閉じているか
  • rightEyeClosed - 右目が閉じているか

ここで注意したいのは、CoreImage は、左下の座標が (0,0) となる点です。よって、検出元画像の上に部品を載せたい場合は、座標をUIKitの座標系に変換する必要があります。

var transform = CGAffineTransformMakeScale(1, -1);
transform = CGAffineTransformTranslate(transform,
                                       0,
                                       -self.imageView.bounds.size.height);
// UIKit座標系に変換
let faceRect = CGRectApplyAffineTransform(feature.bounds,
                                          transform)

これで顔検出ができました。

顔検出というと難しそうなイメージが有りますが、座標系のところ以外は、意外と簡単に使用できますね。

サンプル

https://github.com/koogawa/iSensorSwift/blob/master/iSensorSwift/Controller/FaceDetectionViewController.swift

f:id:koogawa:20131130175727p:plain

補足

【Tips】iOSで歩数をカウントする(Swift編)

数年前に書いた下記記事が古くなってきたので、Swift編 として書き直しました。

【Tips】iOSで歩数をカウントする - koogawa blog

f:id:koogawa:20160423122813p:plain


CoreMotionを使って、歩数カウントを取得する方法をメモしておきます。

動作環境

Xcode 8.0 + Swift 2.2

実装方法

まずは「CoreMotion.framework」を追加します。

f:id:koogawa:20131122152458p:plain

ヘッダをインポートします。

import CoreMotion

歩数をカウントする

歩数をカウントするためには CMPedometer を使います。

iOS 7で登場した CMStepCounteriOS 8 で早くも deprecated になりました。早かったですね…

まずはインスタンスを生成します。メンバ変数にしないとうまくいかないので注意です。

let pedometer = CMPedometer()

startPedometerUpdatesFromDate メソッドで歩数のカウントを開始します。

if CMPedometer.isStepCountingAvailable() {
    self.pedometer.startPedometerUpdatesFromDate(NSDate(), withHandler: {
        [weak self] (data: CMPedometerData?, error: NSError?) -> Void in
        // 歩数が更新されるたびに呼ばれる
        dispatch_async(dispatch_get_main_queue(), {
            if data != nil && error == nil {
                self?.stepLabel.text = "step: \(data!.numberOfSteps)"
            }
        })
    })
}
  • 第1引数:どの時点からのデータを呼び出すか指定(今回は現在日時を指定)
  • 第2引数:歩数更新時に呼び出す handler

第2引数で指定する handler には、計測を始めてからの累計歩数、移動距離など様々な情報が返ってきます。詳細はアップルの公式ドキュメントを参照してください。

歩数カウントが不要になったら、忘れずにカウントを停止しておきます。

self.pedometer.stopPedometerUpdates()

アクティビティの変化を取得する

また、歩数以外にもユーザのアクティビティ情報(歩いているのか、走っているか等)も取得できます。

まずはインスタンスを生成します。メンバ変数にしないとうまくいかないので注意です。

let activityManager = CMMotionActivityManager()

取得を開始します。

if CMMotionActivityManager.isActivityAvailable() {
    self.activityManager.startActivityUpdatesToQueue(NSOperationQueue.mainQueue(),
        withHandler: {
        [weak self] (data: CMMotionActivity?) -> Void in
        dispatch_async(dispatch_get_main_queue(), {
            // アクティビティが変化するたびに呼ばれる
        })
    })
}

アクティビティは CMMotionActivity というクラスで返ってきます。このクラスには次のプロパティが含まれます。

プロパティ 意味
NSDate startDate アクションが発生した時間
CMMotionActivityConfidence confidence データの精度(Low/Medium/High のいずれか)
BOOL stationary 静止状態
BOOL walking 徒歩中
BOOL running ランニング中
BOOL automotive 自動車や電車等の乗り物に乗っている状態
BOOL unknown 不明なアクティビティ

どれかひとつの動作が true になるというわけではなく、例えばautomotivestationary が同時に true になる状況もありえるようです。

最後は忘れずに停止しておきます。

self.activityManager.stopActivityUpdates()

サンプル

注意点

リンク

2016/4/20 #potatotips #28 (iOS/Android開発Tips共有会) に参加してきたよ

昨日は南青山にあるエウレカさんで開催された potatotips #28 (iOS/Android開発Tips共有会) に参加してきました。

potatotips.connpass.com

とても綺麗なオフィスでした。

f:id:koogawa:20160421004042p:plain

いつものようにツイートもまとめておきました。

2016/4/20 #potatotips (iOS/Android開発Tips共有会) 第28回 - Togetter

Androidの発表内容については、すでに id:konifar さんが素晴らしいまとめを公開されています。

konifar.hatenablog.com

以下はiOSに関する発表のメモになります。間違いなどあれば教えて下さい。


PlaygroundでカスタムUIのプロトタイピング

milkit さんによる発表。Xcode の Playground についてのお話です。

Playgroundは日々進化しており、なんとたった3行で TableViewController が表示できてしまう。そして、アニメーションや AutoLayout にも対応している。更に、Xcode 7.3からタップイベントも取れるようになったんだとか。

Debug Remote / Local Notification on watchOS

shingt さんによる発表。watchOS 上で扱う Local Notification のお話です。

watchOS における Remote Notification のテストは、Xcode に仕組みが用意されているため割と簡単。それに対し、Local Notification は仕組みがないのと、引数が UILocalNotification オブジェクトで渡ってくるので、テストがとても大変。そこで、Remote/Local Notification のラッパーを作ったという素敵なお話でした。

RxSwiftのドライバー

Motoki Narita さんによる発表。RxSwift の Driver についてのお話でした。

アカウント登録画面を例に、入力されたユーザ名がすでに使われているかどうかを Validate するサンプルを使って Driver の解説をされていました。

Swiftにわか勢な自分がMacアプリを作ってRxSwiftを使えるようになった話

y.imajo さんによる発表。Macアプリを作ってRxSwiftを使えるようになった話でした。

発表が始まった途端、スクリーンにハッシュタグ #potatotips のツイートが流れているな〜と思っていたら、実はそれ自体が今回発表するMacアプリだった!という驚きの内容でした。

今回のMacアプリのソースコードGitHubに公開されているそうです。

github.com

手を動かさないとRxSwiftは理解できない

これはその通りだなぁ、と思いました。

Repository pattern in Swift

naoty さんによる発表。リポジトリパターンのお話です。

リポジトリパターンを使うことで、オブジェクトをWebAPI経由で取得するのか?Realmから取得するのか?などを知る必要がなくなり、疎結合なコードを書くことが可能になります。今回は「レシピデータ」をリポジトリを介して取得する処理をSwiftで実装する実践的な内容でした。

github.com

tvOS tips

koutalou さんによる発表。tvOSのお話です。最近、マネーフォワードのtvOSアプリを作られたそうです!

個人的には tvOS にも TabBarController と NavigationBar の概念があったことや、WebViewが使いないことを知らなかったので色々と勉強になりました。

国際カンファレンスに登壇する - 応募編

shu223 さんこと堤さんによる発表。堤さんは国際カンファレンスである UIKonf / iOSCon に登壇されるそうです!🎉

発表の中では、応募する理由、参加することのメリット、カンファレンスの探し方など、なかなか聞くことのできない貴重なお話を聞くことができました。

スライドなどは堤さんのブログにまとめられているようです。

d.hatena.ne.jp

iOS Universal Linkについて

最近ボルダリングにハマっているという TachibanaKaoru さんによる発表。iOS 9 から使えるようになった Universal Link のお話でした。

Universal Link の仕組み自体は知っていたのですが、SFSafariViewController, WKWebView, UIWebView の中では無効になる事実や、自分でURLを入力した場合も無効になるというTipsが知れたのは良かったです。

また、Smart App Banner との違いについても触れられていました。Universal Link と Smart App Banner は目的が違うものになるため、別々に実装する必要があるとのことです。

所感

ポテチでもRxの発表が増えてきており、流行ってるんだなぁ〜というのを再認識しました。

また、発表中も程よいボリュームでBGMが流れていて良いな、と思いました。

現場からは以上です。*1

*1:こにふぁーさんをリスペクトしてみました。怒られたら消します

【Tips】iOSでマイクの音を検知する(Swift編)

数年前に書いた下記記事が古くなってきたので、Swift編 として書き直しました。

f:id:koogawa:20160417130548p:plain


Core Audioを使用して、マイクから音を検知する方法をメモしておきます。

実行環境

  • Xcode 7.3
  • Swift 2.2
  • Deployment Target iOS 8

実装方法

ヘッダをインポートします。

import AudioToolbox

音声入力用のキューと監視タイマーを準備しておきます。

var queue: AudioQueueRef!
var timer: NSTimer!

記録するデータフォーマットを決めます。

var dataFormat = AudioStreamBasicDescription(
    mSampleRate: 44100.0,
    mFormatID: kAudioFormatLinearPCM,
    mFormatFlags: AudioFormatFlags(kLinearPCMFormatFlagIsBigEndian |
        kLinearPCMFormatFlagIsSignedInteger |
        kLinearPCMFormatFlagIsPacked),
    mBytesPerPacket: 2,
    mFramesPerPacket: 1,
    mBytesPerFrame: 2,
    mChannelsPerFrame: 1,
    mBitsPerChannel: 16,
    mReserved: 0)

▼各項目の意味

データ型 メンバ名 意味
Float64 mSampleRate サンプリング周波数(1秒間のフレーム数)
UInt32 mFormatID フォーマットID(リニアPCM、MP3、AACなど)
UInt32 mFormatFlags フォーマットフラグ(エンディアン、整数or浮動小数点数
UInt32 mBytesPerPacket 1パケット(データを読み書きする単位)のバイト数
UInt32 mFramesPerPacket 1パケットのフレーム数
UInt32 mBytesPerFrame 1フレームのバイト数
UInt32 mChannelsPerFrame 1フレームのチャンネル数
UInt32 mBitsPerChannel 1チャンネルのビット数
UInt32 mReserved

音の監視を開始します。

var audioQueue: AudioQueueRef = nil
var error = noErr
error = AudioQueueNewInput(
    &dataFormat,
    AudioQueueInputCallback,
    UnsafeMutablePointer(unsafeAddressOf(self)),
    .None,
    .None,
    0,
    &audioQueue)
if error == noErr {
    self.queue = audioQueue
}
AudioQueueStart(self.queue, nil)

今回は音を検知するだけで、録音の必要はないので AudioInputCallback は空にしておきます。

private func AudioQueueInputCallback(
    inUserData: UnsafeMutablePointer<Void>,
    inAQ: AudioQueueRef,
    inBuffer: AudioQueueBufferRef,
    inStartTime: UnsafePointer<AudioTimeStamp>,
    inNumberPacketDescriptions: UInt32,
    inPacketDescs: UnsafePointer<AudioStreamPacketDescription>)
{
    // Do nothing, because not recoding.
}

マイクのレベルを取得できるように、レベルメータを有効化しておきます。

var enabledLevelMeter: UInt32 = 1
AudioQueueSetProperty(self.queue,
                      kAudioQueueProperty_EnableLevelMetering,
                      &enabledLevelMeter,
                      UInt32(sizeof(UInt32)))

一定時間ごとにマイクレベルを監視します。ここでは NSTimer を使用しました。

self.timer =
    NSTimer.scheduledTimerWithTimeInterval(0.5,
                                           target: self,
                                           selector: #selector(AudioViewController.detectVolume(_:)),
                                           userInfo: nil,
                                           repeats: true)
self.timer.fire()

一定時間ごとにレベルを取得して、画面に表示します。

func detectVolume(timer: NSTimer)
{
    // Get level
    var levelMeter = AudioQueueLevelMeterState()
    var propertySize = UInt32(sizeof(AudioQueueLevelMeterState))

    AudioQueueGetProperty(
        self.queue,
        kAudioQueueProperty_CurrentLevelMeterDB,
        &levelMeter,
        &propertySize)

    // Show the audio channel's peak and average RMS power.
    self.peakTextField.text =
        "".stringByAppendingFormat("%.2f", levelMeter.mPeakPower)
    self.averageTextField.text =
        "".stringByAppendingFormat("%.2f", levelMeter.mAveragePower)

    // Show "LOUD!!" if mPeakPower is larger than -1.0
    self.loudLabel.hidden = (levelMeter.mPeakPower >= -1.0) ? false : true
}

通常、mPeakPowermAveragePower には負の値がセットされており、音量に比例して値も大きくなっていきます。最大値は 0 になります。

最後に忘れずにキューを空にして停止します。

AudioQueueFlush(self.queue)
AudioQueueStop(self.queue, false)
AudioQueueDispose(self.queue, true)

サンプルコード

注意点

  • ユーザがマイクの使用を許可していない場合、音の検知はできません

応用できそうなアプリ

  • 声や拍手などに反応するゲームアプリなど
  • iPhoneに息を吹きかけると何かがめくれるアプリ(実際にありましたねw)

参考にさせて頂いたリンク

LINE BOT API で bot を作ったときにハマったことメモ

LINEBOT APIが公開されたので、簡単な検索botを作ってみました。 botの作り方についてはすでにたくさんの良記事が公開されているので、ここでは主にハマりやすいポイントについてメモしておきます。

開発環境

アカウント登録

下記サイトからBOT API Trial Accountを登録します。

https://business.line.me/services/products/4/introduction

アカウントは1人あたり1つまで申し込めます。先着10,000名様まで利用可能だそうです。(4/9 17:00時点ではまだ申し込めるようです)

注意点

  • 友だち数上限は50人までです
  • 料金は無料です

実装方法

素晴らしい解説記事がすでに公開されているので、ここではリンクを貼るだけにしておきます。先人たちに感謝🙇🏻

ハマったポイント

Callback URLはHTTPS必須

公式ドキュメント

The callback URL must use HTTPS.

とあるように、Callback URLは HTTPS を利用する必要があります。HTTPで登録しようとするとエラーになります。

ポート番号を443にする必要がある

公式ドキュメントではあまり強調されていないのですが、Callback URLにはポート番号を指定する必要があります。

例:https://example.com:443/callback

Callback先をホワイトリストに登録しておく必要がある

Callback先のサーバーを予め「Server IP Whitelist」に登録しておかないと、サーバーからBOT APIをたたくことができません(StatusCode:427が返ってくる)。ホワイトリストはLINE developersの画面から登録することができます。

f:id:koogawa:20160409172820p:plain

Callback URLにリクエストがこない問題

サーバ側で使用しているSSL証明書によってはCallback URLにリクエストが通らない例があるようです。Twitterを見ると Let's Encrypt の証明書と相性が悪いようです。

成果物

キーワードをつぶやくと、それに関するノウハウ記事を教えてくれるbotを作りました。

f:id:koogawa:20160409173934p:plain

こちらから友達登録できます。(予告なく停止する可能性があります)

f:id:koogawa:20160409174015p:plain

リンク

【Tips】iOSで電子コンパスを使う(Swift編)

数年前に書いたこちらの記事が古くなってきたので、Swift編 として書き直しました。

blog.koogawa.com


f:id:koogawa:20160402095838p:plain

磁力センサーを利用し、iPhoneの向きを計測する方法を解説します。

「磁北」と「真北」について

単純に「北」と言っても、「磁北(じほく)」と「真北(しんぽく)」の二種類が存在します(2つの違いについては「真北とは? | 用語集とGISの使い方 | 株式会社パスコ」で丁寧に解説されています)。

ここでは磁北を取得する方法について解説します。

実行環境

実装方法

まずは「CoreLocation.framework」を追加します。

f:id:koogawa:20131122152458p:plain

次に Info.plist の NSLocationWhenInUseUsageDescription位置情報を使用する目的 を記載します。この内容はユーザに位置情報の使用を許可する際に表示されます。

f:id:koogawa:20160326145627p:plain

CoreLocation をインポートしましょう。

import CoreLocation

CLLocationManagerDelegate プロトコルを宣言します。

class ViewController: UIViewController, CLLocationManagerDelegate {

ユーザが位置情報の使用を許可しているか確認します。許可状況は didChangeAuthorizationStatus デリゲートで確認できます。初回は NotDetermined ステータスが返るので、requestWhenInUseAuthorization() メソッドでユーザに許可を求めます。

func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
    switch status {
    case .NotDetermined:
        locationManager.requestWhenInUseAuthorization()
    case .Restricted, .Denied:
        break
    case .Authorized, .AuthorizedWhenInUse:
        break
    }
}

これで準備が整いました。さっそく、ヘディングイベントの取得を開始してみましょう。

if CLLocationManager.locationServicesEnabled() {
    locationManager = CLLocationManager()
    locationManager.delegate = self
    
    // 何度動いたら更新するか(デフォルトは1度)
    locationManager.headingFilter = kCLHeadingFilterNone
    
    // デバイスのどの向きを北とするか(デフォルトは画面上部)
    locationManager.headingOrientation = .Portrait

    locationManager.startUpdatingHeading()
}

この状態でiPhoneの向きが変わると、次のメソッドが呼ばれます。

func locationManager(manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    self.textField.text = "".stringByAppendingFormat("%.2f", newHeading.magneticHeading)
}

magneticHeading には 0.0 から 359.9 の値が入ります。北の方向がちょうど 0.0 になり、それから東、南、西の順に値が増えていき、北に戻ると再び 0.0 に戻ります。

方角
0.0
90.0
180.0
西 270.0

ヘディングイベントの取得を停止するには次のメソッドを実行します。

if CLLocationManager.locationServicesEnabled() {
    locationManager.stopUpdatingHeading()
}

イベントの取得が不要になったら、忘れずに停止しておきましょう。

サンプルコード

注意点

応用できそうなアプリ

参考リンク