Оглавление
Глава 10. Отслеживание движения и ориентации в SwiftUI
Отслеживание вращения с помощью гироскопа
Обнаружение перемещения устройства
Глава 10. Отслеживание движения и ориентации в SwiftUI
Мобильные устройства, такие как iPhone или iPad – это полноценные мобильные компьютеры, которые мы носим с собой в кармане или в сумке, где бы мы не были. Однако в отличие от настольных компьютеров или ноутбуков, мобильные компьютеры могут отслеживать движение и положение в пространстве. Используя Apple API задача отслеживания перемещений рук пользователя в играх или измерение углов положения девайса в пространстве – для Вас будет весьма проста в выполнении.
Для отслеживания перемещения или ориентации в пространстве, каждое iOs устройство поставляется с встроенным акселерометром, который может отслеживать перемещение. В дополнение к акселерометру, iOs устройства также содержат гироскоп для определения положения в пространстве iOs устройства. Добавив отслеживание и определения положения в пространстве, Вы можете создавать интересные приложения, которые отвечают на физические жесты также быстро и точно, как и на жесты прикосновений.
Что такое Core Motion?
Если Вы хотите отслеживать что то, помимо простых перемещений, компания Apple предоставила программный фреймворк, называемый Core Motion(Ядро перемещений). Core Motion предоставляет программный доступ для отслеживания следующих типов пермещений:
- Ускорение в трех измерениях
- Вращение вокруг осей x, y, z
- Датчик отслеживания магнитных полей, который отслеживает положение устройства по отношению к магнитному полю Земли
- Датчик перемещения устройства относительно гравитации
Внимание! Для тестирования перемещения и ориентации, Вам нужно реальное iOs устройство подключенное к Вашему Macintosh через USB кабель. Симулятор и панель Canvas не определяют изменения в физическом перемещении эмулятора в пространстве.
Для того, чтобы использовать Core Motion в приложении, необходимо импортировать фреймворк CoreMotion и затем создать объект типа CMMotionManager:
import CoreMotion let motionManager = CMMotionManager()
И необходимо создать очередь, куда будут приходить обновления с датчиков – OperationQueue
. Без использования очереди, данные о положении и перемещении устройства могут поступать даже быстрее, чем приложение способно обработать физически, что приведет к подвисанию приложения или снижении быстродействия Вашего устройства. Определяется очередь как константа:
let queue = OperationQueue()
Акселерометр, iOs и SwiftUI
Акселерометр может измерять сразу ускорение и силу тяжести в трех измерениях. Акселерометр может определять не только как iOs устройство держат в руках, но также и то, какой стороной(например лицевой или тыльной) устройство лежит на плоской поверхности(такой как например стол). Акселерометр измеряет g-силу(g– сила ускорения свободного падения), так значение 1.0 возвращается, если акселерометр измерил что сила 1g воздействует в конкретном направлении, как например:
- Если устройство находит в идеальном портретном положении, будет получено значение равное силе 1g по оси y.
- Если устройство держится под углом, то сила 1g будет равномерно распределена по осям, по которым идет смещение положения телефона от идеального. Если телефон держится под углом 45 градусов, сила 1g будет равномерно распределена между двумя осями.
Внезапное перемещение может быть отслежено акселерометром, если получаемые значения получаются значит больше, чем 1g. При нормальном использовании, акселерометр не получает значительно большие значения прикладываемой силы, чем 1g, по одной или даже несколько осях, как указано на рисунке 10-1.
Пришло время посмотреть как программно работать с акселерометром в SwiftUI за 12 шагов.
- Создадим новое iOs приложение и назовем его Chapter10AccelerateApp
- Откроем файл ContentView в панели Навигатора
- Добавим импорт библиотеки CoreMotion
import CoreMotion
- Добавим объект, ответственный за получение данных с акселерометра, очередь для приема данных и 3 State переменные для обозначения осей:
let motionManager = CMMotionManager() let queue = OperationQueue() @State private var x: Double = 0.0 @State private var y: Double = 0.0 @State private var z: Double = 0.0
- В теле var body: some View удалим весь сгенерированный код и добавим VStack компонент с тремя виджетами Text
VStack{ Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }
Эти 3 Text компонента отображает изменение значений, отвечающих за перемещение устройства по 3-м осям.
- В конце блока VStack добавим модификатор .onAppear со следующим кодом:
.onAppear{ motionManager.startAccelerometerUpdates(to: queue){ (data: CMAccelerometerData?, error: Error?)in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMAcceleration = data.acceleration motionManager.accelerometerUpdateInterval = 2.5 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } }
.startAccelerometerUpdates
проверяет, есть ли доступ к акселерометру. Если есть – функция получает обновления данных с датчиков и сохраняет их в константеtrackMotion
. Для избежания проблем с зависанием мы используем GrandCentralDispatch в главном потоке – так как переменные x,y,z имеют доступ к UI интерфейсу.Для того, чтобы не нагружать процессор устройства слишком сильно, мы устанавливаем период получения обновлений с акселерометра раз в 2.5 секунды путем установления
.accelerometerUpdateInterval
значения, равному 2.5, но Вы можете установить любое значение, какое Вам нужно. Исходный код ContentView выглядит так:import SwiftUI import CoreMotion struct ContentView: View { let motionManager = CMMotionManager() let queue = OperationQueue() @State private var x: Double = 0.0 @State private var y: Double = 0.0 @State private var z: Double = 0.0 var body: some View { VStack{ Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }.onAppear{ motionManager.startAccelerometerUpdates(to: queue){ (data: CMAccelerometerData?, error: Error?)in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMAcceleration = data.acceleration motionManager.accelerometerUpdateInterval = 2.5 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- Подсоедините iPhone к Вашему Mac через USB кабель, разблокируйте телефон и добавьте Ваш Mac в список доверенных компьютеров на iPhone.
- Выберите Product – Destination и выберите из списка девайсов Ваше iOs устройство, как показано на рисунке 10-2. Вы также можете нажать на текущее выбранное устройство, отображаемое в верхней части окна Xcode для отображения того же самого списка iOs устройств(см. Рисунок 10-3)
- Выполните сборку и установку приложения на Ваше устройство, после того как оно откроется, поставьте Ваше iOs устройство на плоский стол. Значение z должно быть или 1 или -1. Как Вы Видите на рисунке 10-4, реальные значения не доходят до единицы по модулю. Если у Вас возникла ошибка при запуске приложения на iOs устройстве – “Ненадежный разработчик” – посмотрите здесь, как решить эту проблему.
- Возьмите смартфон в руки и постарайтесь держать его строго в портретном режиме. Показания y переменной будут 1 или -1(если быть точнее, то около этих значений)
- Поставьте телефон в ландшафтный режим. Значение x переменной будет в районе значений от 1 до -1.
- Нажмите на иконку Stop в среде XCode для остановки приложения. Исходный код Вы можете посмотреть и скачать также с нашего GitHub.
Отслеживание вращения с помощью гироскопа
Гироскоп отслеживает вращение и ориентацию вокруг осей x, y, z. Для начала посмотрите еще раз на рисунок 10-1. У нас то же направление осей, что и при работе с акселерометром. Вращение вокруг оси x возникает, когда мы наклоняем устройство вперед и назад относительно горизонтальной линии в центре устройства, проходящей параллельно земле. Вращение вокруг оси y возникает когда мы поворачиваем устройство относительно вертикальной линии, проходящей к центру земли через середину устройства. Вращение по оси z происходит когда мы вращаем устройство по часовой или против часовой стрелке через линию, проходящую через центр устройства, словно протыкающую девайс по направлении от нас к противоположной стороне.
Рассмотрим как работать с гироскопом в SwiftUI:
- Создадим новое iOs приложение и назовем его Chapter10GyroApp
- Откроем файл исходного кода ContentView в панели Навигатора
- Добавим импорт библиотеки CoreMotion
import CoreMotion
- В структуре ContentView добавим 3 State переменные, отвечающие за оси в пространстве, а также очередь принимаемых данных с датчика и сам объект CMMotionManager для доступа к датчику – гироскопу
let motionManager = CMMotionManager() let queue = OperationQueue() @State private var x: Double = 0.0 @State private var y: Double = 0.0 @State private var z: Double = 0.0
- В теле UI структуры добавим контейнер
VStack
с тремя компонентамиText
VStack{ Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }
Эти виджеты
Text
отображают изменение значений вращений устройства по осям x, y, z при воздействии пользователя на телефон - Добавим модификатор
.onAppear
в конец блока VStack:.onAppear{ motionManager.startGyroUpdates(to: queue) { (data : CMGyroData?, error : Error?) in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMRotationRate = data.rotationRate motionManager.gyroUpdateInterval = 2.5 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } }
startGyroUpdates
вначале проверяет доступ к гироскопу. Если доступен, то получает и сохраняет данные в константеtrackMotion
. Далее при помощи Grand Central Dispatch мы асинхронно обновляем значения нашихState
переменных в главном потоке. Мы взяли период получения обновления значений с гироскопа – раз в 2.5 секунды, что в целом довольно медленно. Сделали мы это для того, чтобы не напрягать вычислительные способности нашего устройства и не разряжать батарею чрезмерно.Исходный код нашего файла ContentView приведен ниже:
// // ContentView.swift // Chapter10GyroApp // // Created by Timur on 16.04.2023. // import SwiftUI import CoreMotion struct ContentView: View { let motionManager = CMMotionManager() let queue = OperationQueue() @State private var x: Double = 0.0 @State private var y: Double = 0.0 @State private var z: Double = 0.0 var body: some View { VStack{ Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }.onAppear{ motionManager.startGyroUpdates(to: queue) { (data : CMGyroData?, error : Error?) in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMRotationRate = data.rotationRate motionManager.gyroUpdateInterval = 2.5 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- Подсоеденим наше iPhone устройство к Mac через USB кабель, а после разблокируем наше устройство
- Выбрав в среде XCode наше физическое устройство, куда мы будем устанавливать приложение в меню Product – Destination или в верхней панели устанавливаемых устройств(см. рисунок 10-2 и 10-3), установим наше приложение Chapter10GyroApp на физическое устройство.
- После того как приложение откроется, попробуйте быстро вращать устройство вокруг горизонтальной оси(ось x). Вы увидите на дисплее телефона, что значение переменной x резко меняется от -8 до 10.
- Точно также попробуйте быстро покрутить устройство относительно вертикальной оси. Теперь вы заметите, что значение y меняется с около нулевых значений до диапазона от 7 до -6.
- Вращайте ваше iOs устройство по часовой и против часовой стрелке вокруг z оси. Теперь Вы увидите, что значение z переменной изменяется до значений в диапазоне от 5 до -6.
- Нажмите на кнопку Stop в среде Xcode для остановки приложения
Измеряем магнитные поля
Магнитометр iOs устройства измеряет отношение магнитного поля Земли к iOs девайсу. Возвращаемые значения содержат измерение магнитного поля Земли в микротеслах, где значение x означает горизонтальное смещение к ближайшему магнитному полю, y значение содержит вертикальное смещение и z значение содержит измерение высоты относительно магнитного поля Земли.
Посмотрим, как получать данные с магнитометра:
- Создадим новое iOs приложение и дадим ему название Chapter10MagnetApp
- Откроем файл ContentView в панели Навигатора
- Добавим импорт фреймворка CoreMotion
import CoreMotion
- Как обычно добавим 3
State
переменные для отображения значений по 3-ом осям, а также очередь приема сообщений и сам объектCMMotionManager()
для работы с магнитометромlet motionManager = CMMotionManager() let queue = OperationQueue() @State private var x : Double = 0.0 @State private var y : Double = 0.0 @State private var z : Double = 0.0
- Удалим весь код, сгенерированный в теле структуры и добавим контейнер
VStack()
с тремяText
элементами:VStack { Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }
Эти 3
Text
компонента отображают изменение значений магнитных полей по 3-м осям. - Добавим модификатор
.onAppear
в конец контейнераVStack
:.onAppear{ motionManager.startMagnetometerUpdates(to: queue){ (data: CMMagnetometerData?, error: Error?) in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMMagneticField = data.magneticField motionManager.magnetometerUpdateInterval = 1.0 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } }
Метод
.startMagnetometerUpdates
проверяет, доступен ли магнитометр. Если доступен, получает данные с магнетометр и сохраняет их в константеtrackMotion
. Обновление переменных происходит асинхронно через Grand Central Dispatch в главном потокеИтоговый файл
ContentView
выглядит следующим образом:// // ContentView.swift // Chapter10MagnetApp // // Created by Timur on 16.04.2023. // import SwiftUI import CoreMotion struct ContentView: View { let motionManager = CMMotionManager() let queue = OperationQueue() @State private var x : Double = 0.0 @State private var y : Double = 0.0 @State private var z : Double = 0.0 var body: some View { VStack { Text("x: \(x)") Text("y: \(y)") Text("z: \(z)") }.onAppear{ motionManager.startMagnetometerUpdates(to: queue){ (data: CMMagnetometerData?, error: Error?) in guard let data = data else{ print("Error: \(error!)") return } let trackMotion: CMMagneticField = data.magneticField motionManager.magnetometerUpdateInterval = 1.0 DispatchQueue.main.async { x = trackMotion.x y = trackMotion.y z = trackMotion.z } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- Подключите iPhone к Вашему Mac через USB кабель и разблокируйте Ваш iPhone.
- Выберите Product – Destination и выберите iOs устройство подсоединенное к Вашему Mac
- Походите и покрутите Ваш телефон, чтобы посмотреть как изменяются значения магнитных полей.
- Нажмите кнопку Stop в Xcode и остановите приложение
Обнаружение перемещения устройства
Под словом – перемещение устройства, мы обычно понимаем получение целой системы данных – крен, тангаж и рысканье. Для тех читателей, кто играл в воздушные симуляторы, это уже знакомые термины. Если кратко, то под креном подразумевается вращение устройства вокруг вертикальной оси, под термином тангаж подразумевается вращение устройства вокруг горизонтальной оси и под рысканьем в таком случае идет речь вращение вокруг оси проходящей через центр устройства от наблюдателя к устройству. Вы можете посмотреть иллюстрацию этих понятий на рисунке 10-5.
Создадим простое iOs приложение чтобы разобраться на практике как получить значение крена, тангажа и рысканья на устройстве:
- Создадим новое iOs приложение и назовем его Chapter10DeviceMotionApp
- Откроем файл ContentView в панели Навигатора
- Импортируем библиотеку CoreMotion в наш проект:
import CoreMotion
- Добавим 3
State
переменные для отображения текущих значений крена, тангажа и рысканья. А также очередь для хранения поступающих данных с датчиков движенияlet motionManager = CMMotionManager() let queue = OperationQueue() @State private var pitch: Double = 0.0 @State private var yaw: Double = 0.0 @State private var roll: Double = 0.0
- Удалим все сгенерированные элементы UI и добавим контейнер VStack с 3-мя нам уже знакомыми компонентами Text
VStack { Text("Pitch: \(pitch)") Text("Yaw: \(yaw)") Text("Roll: \(roll)") }
Эти 3 компонента
Text
отображают текущие значение наклонов и поворотов устройства в различных направлениях. - Добавим модификатор
.onAppear
в конец контейнераVStack
:.onAppear{ motionManager.startDeviceMotionUpdates(to: queue) {(data: CMDeviceMotion?, error: Error?) in guard let data = data else{ print("Error: \(error)") return } let trackMotion : CMAttitude = data.attitude motionManager.deviceMotionUpdateInterval = 1.0 DispatchQueue.main.async { pitch = trackMotion.pitch yaw = trackMotion.yaw roll = trackMotion.roll } } }
startDeviceMotionUpdates
проверяет, доступен ли сейчас акселерометр. Если доступен, мы получаем с него данные и сохраняем в константеtrackMotion
. Для того, чтобы наш UI не страдал от зависаний и подтормаживаний во время работы приложения, мы будем асинхронно обновлять наш UI через GrandCentralDispatch в главном потоке. Время обновления значений мы выбрали – 1 раз в 1 секунду. Исходный код файлаContentView
выглядит следующим образом:// // ContentView.swift // Chapter10DeviceMotionApp // // Created by Timur on 16.04.2023. // import SwiftUI import CoreMotion struct ContentView: View { let motionManager = CMMotionManager() let queue = OperationQueue() @State private var pitch: Double = 0.0 @State private var yaw: Double = 0.0 @State private var roll: Double = 0.0 var body: some View { VStack { Text("Pitch: \(pitch)") Text("Yaw: \(yaw)") Text("Roll: \(roll)") }.onAppear{ motionManager.startDeviceMotionUpdates(to: queue) {(data: CMDeviceMotion?, error: Error?) in guard let data = data else{ print("Error: \(error)") return } let trackMotion : CMAttitude = data.attitude motionManager.deviceMotionUpdateInterval = 1.0 DispatchQueue.main.async { pitch = trackMotion.pitch yaw = trackMotion.yaw roll = trackMotion.roll } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
- Подсоедините Ваш iPhone к Mac через USB кабель и разблокируйте Ваш iPhone
- В XCode выберите Ваше устройство как целовое для установки приложения.
- Покрутите Ваше устройство вокруг вертикальной оси. Значение Roll должно меняться от -2 до 3.
- Подбросьте вверх ваш девайс(не сильно). Значение тангажа будет менять от 1 до -2.
- Покрутите ваш девайс по часовой и против часовой стрелки. Значение рысканья будет менять от -2 до 1
- Нажмите на кнопку Stop в среде Xcode для остановки выполнения приложения.
Заключение
Каждое iOs устройство поставляется со встроенными датчиками для измерения перемещения. Для отслеживания перемещения любого iOs устройства, Вам необходимо использовать фреймворк CoreMotion.
iOs устройство в состоянии отслеживать: ускорение, вращение и даже магнитное поле Земли. Отслеживание перемещения iOs устройства позволяет приложению реагировать должным образом на перемещение в пространстве и подходить к управлению и взаимодействию с приложением совсем другим образом.
Как мы убедились на наших примерах, работать с датчиками в среде iOs одно наслаждение – код предельно простой, лаконичный и понятный для программистов даже начального уровня.