Gluegent Blog

Gluegent Blog

加速度センサーを用いた iOSアプリを開発して遊んでみた

  • その他Gluegent製品
  • グルージェントフロー
  • 技術

こんにちは、エンジニアのYuito Hayashiです。

みなさんは「グルージェントフロー(Gluegent Flow)」と「共有アドレス帳」にスマートフォンアプリがあることをご存じですか?
最近、私は業務でこれらのiOSアプリの開発保守などに携わっています。
そのため、iOSアプリによく触れているので今回はちょっとしたiOSアプリを開発して遊んで、それについて記事にしていこうと思います。

加速度センサーを用いた iOSアプリを開発して遊んでみた

開発環境

開発環境はXcode 16.4で言語はSwift 5を使用します。
また、インターフェースはStoryboardではなく、SwiftUIで開発することにしました。

どんなアプリを作ろうか考えたのですが、せっかくiPhoneには高性能な様々なセンサーが搭載されているので、今回は加速度センサーを用いたアプリを作成してみようと思います。

加速度の値を取得してみる

まずは、加速度の値を取得し、表示させるiOSアプリを作ってみようと思います。
加速度はCoreMotionというフレームワークに含まれるCMMotionManagerクラスを使うことで取得することができます。
以下のコードから取得した3軸加速度の値を画面に表示させることができます。

import SwiftUI
import CoreMotion

class MotionManager: ObservableObject {
    private var phoneMotion = CMMotionManager()
    @Published var x: Double = 0.0
    @Published var y: Double = 0.0
    @Published var z: Double = 0.0
    
    func start() {
        phoneMotion.startAccelerometerUpdates(to: .main) { [weak self] data, _ in
            guard let data else { return }
            self?.x = data.acceleration.x
            self?.y = data.acceleration.y
            self?.z = data.acceleration.z
        }
    }
    
    func stop() {
        phoneMotion.stopAccelerometerUpdates()
    }
}

struct ContentView: View {
    @StateObject private var motion = MotionManager()
    
    var body: some View {
        VStack {
            Text(String(format: "x軸: %.3f", motion.x))
            Text(String(format: "y軸: %.3f", motion.y))
            Text(String(format: "z軸: %.3f", motion.z))
        }
        .font(.system(size: 28, design: .rounded))
        .onAppear { motion.start() }
        .onDisappear { motion.stop() }
    }
}

以下が実行画面になります。
iPhoneを傾けるとリアルタイムで数値が変わるのがわかります。

スマートフォンの加速度センサーの値を表示するアプリの動作画面
スマートフォンの加速度センサーの値を表示するアプリの動作画面

スマホの落下を検知してみる

次に、加速度の値を用いてスマホの落下を検知してみようと思います。
自由落下すると合計の加速度がほぼ0になるはずなので、合計加速度の値が0.1以下になった間隔が0.2秒を超えたときスマホが落下していると判定することにしてみます。
そして、落下と判定されたらスマホをバイブレーションさせて、アラートを表示させるようにしてみたいと思います。
スマホのバイブレーションはAudioToolboxフレームワークを使うことで振動させることができます。
以下のコードからスマホの落下検出ができます。(コードは一部省略してます。)

import AudioToolbox

struct ContentView: View {
    @StateObject private var motion = MotionManager()
    @State private var showAlert = false
    @State private var lowGStartDate: Date? = nil
    
    private let timer = Timer.publish(every: 1.0 / 120.0, on: .main, in: .common).autoconnect()
    private let thresholdG: Double = 0.10
    private let minDuration: TimeInterval = 0.20
    
    var body: some View {
        VStack {
            Text(String(format: "x軸: %.3f", motion.x))
            Text(String(format: "y軸: %.3f", motion.y))
            Text(String(format: "z軸: %.3f", motion.z))
        }
        .font(.system(size: 28, design: .rounded))
        .onAppear { motion.start() }
        .onDisappear { motion.stop() }
        .onReceive(timer, perform: { _ in
            detectFall(x: motion.x, y: motion.y, z: motion.z)
        })
        .alert("お前今落としただろ", isPresented: $showAlert) {
            Button("すみません...") { showAlert = false }
        }
    }
    
    func detectFall(x: Double, y: Double, z: Double) {
        let totalG = sqrt(x*x + y*y + z*z)
        if totalG < thresholdG {
            if lowGStartDate == nil {
                lowGStartDate = Date()
            } else if Date().timeIntervalSince(lowGStartDate!) > minDuration {
                AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
                showAlert = true
                lowGStartDate = nil
            }
        } else {
            lowGStartDate = nil
        }
    }
}

以下が実行画面になります。
実際にスマホを空中で手放してキャッチしてみたのですが、うまく動作できていました。

スマートフォンが落下するとアラートが表示されるアプリの動作画面
スマートフォンが落下するとアラートが表示されるアプリの動作画面

スマホを傾けることで画像を動かしてみる

次は、加速度を使って画像を動かす物理シミュレーションっぽいアプリを作ってみようと思います。
画面端に画像が当たったら、ランダムに画像の色を変更してスマホを振動させます。
そして、画像を跳ね返すようにしてみようと思います。

画像は、加速度の値を使用して、座標を更新させることで動かします。
注意点として、iPhoneの座標は中央が原点ではなく、左上が原点になります。
x軸は右に行くほどプラス、y軸は下に行くほどプラスになります。

以下のコードからスマホを傾けることで画像を動かすことができます。(コードは一部省略してます。)
また、画像はAssetsに配置する必要があります。

struct ContentView: View {
    @StateObject private var motion = MotionManager()
    @State private var imagePosition: CGPoint = .zero
    @State private var vx: CGFloat = 0
    @State private var vy: CGFloat = 0
    @State private var tint = Color.orange
    
    private let timer = Timer.publish(every: 1.0 / 120.0, on: .main, in: .common).autoconnect()
    private let imageSize: CGFloat = 100
    private let restitution: CGFloat = 0.5
    
    var body: some View {
        ZStack {
            VStack {
                Text(String(format: "x軸: %.3f", motion.x))
                Text(String(format: "y軸: %.3f", motion.y))
                Text(String(format: "z軸: %.3f", motion.z))
            }
            .font(.system(size: 28, design: .rounded))
            GeometryReader { geometry in
                Image("flow-icon")
                    .renderingMode(.template)
                    .resizable()
                    .frame(width: imageSize, height: imageSize)
                    .position(imagePosition)
                    .foregroundStyle(tint)
                    .onAppear {
                        imagePosition = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
                        motion.start()
                    }
                    .onDisappear { motion.stop() }
                    .onReceive(timer, perform: { _ in
                        imagePosition = nextPosition(current: imagePosition, in: geometry.size)
                    })
            }
        }
    }
    
    func nextPosition(current: CGPoint, in size: CGSize) -> CGPoint {
        vx += CGFloat(motion.x)
        vy -= CGFloat(motion.y)
        
        var newX = current.x + vx
        var newY = current.y + vy
        let half = imageSize / 2
        
        if newX <= half || newX >= size.width - half {
            vx *= -restitution
            newX = min(max(newX, half), size.width - half)
            hitEffect()
        }
        if newY <= half || newY >= size.height - half {
            vy *= -restitution
            newY = min(max(newY, half), size.height - half)
            hitEffect()
        }
        return CGPoint(x: newX, y: newY)
    }
    
    func hitEffect() {
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
        tint = Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
}

以下が実行画面になります。
画面端に当たると色が変わるのはDVDのスクリーンセーバーみたいな感じですね。

スマートフォンを傾けると画像を動かすことができるアプリの動作画面
スマートフォンを傾けると画像を動かすことができるアプリの動作画面

さいごに

今回は加速度センサーを使ったiOSアプリを開発して遊んでみました。
iPhoneのほかにも、AirPodsやApple Watchのような身近なデバイスにも加速度センサーが搭載されています。
次は、これらのデバイスと連携したアプリも作ってみたいです。

最後に、「グルージェントフロー(Gluegent Flow)」と「共有アドレス帳」にスマーフォンアプリがあることを知らなかった、使っていないという方がいましたら是非ダウンロードして使ってみてください!
最後まで読んでいただきありがとうございました!
(Yuito Hayashi)