В прошлой статье мы разобрали работу с 3DTouch на iPhone. Теперь осталось завершить пару штрихов и закончить наше приложение.
Как я уже сказал, основной функционал готов, остались только доработки:
Два первых пункта я не смогу отобразить ни с помощью скриншотов, ни с помощью анимированных gif. Поэтому сделайте дополнения, описанные ниже и проверяете самостоятельно на своих устройствах. А вот третий пункт я продемонстрирую. Но давайте дополнения сделаем все вместе.
Импортируем фреймворк AudioToolbox перед декларацией класса ViewController, а также добавим свойство isPlaySound для исключения многократного проигрывания виброотклика.
Далее внесите изменения в метод touchesMoved(::), чтобы он выглядел следующим образом:
Ничего сложного — при запуске iOS приложения, а в данном случае при инициализации ViewController и создании свойства класса isPlaySound мы включаем возможность проигрывания звуков — в том числе и виброоткликов. При достижении максимальной силы нажатия происходит проверка isPlaySound (1) и если он true то выполняется вибрация и сразу срабатывает запрет (2) на проигрывание вибрации. Этот запрет снимается (3), если сила нажатия становится меньше максимально возможного значения.
Теперь про плавность. Обновления надписей присходит очень быстро, со скоростью срабатывания метода touchesMoved(::), а это несколько сотен срабатываний в секунду. Для уменьшения частоты обновления надписи я добавил свойство класса ViewController isUpdate и поставил наблюдателей свойств didSet.
Суть подобной конструкции в том, что как только мы устанавливаем данное свойство в значение false оно возвращается в значение true через 0,01 секунды. Соответственно при записи текста в UILabel мы будем устанавливать значение свойства isUpdate в значение false и не позволим обновлять надписи пока оно не станет true. Поэтому обновления записей будет происходить у нас не чаще одного раза в сотую доли секунды.
В методе touchesMoved(::) в ветке, где мы выводим показания % силы и веса в граммах измените код следующим образом:
Этого будет достаточно для придания плавности при обновлении надписей
Сначала давайте создадим UIView и сделаем его полупрозрачным и круглой формы. Для этого добавим свойство класса ViewController и сделаем первоначальные настройки в методе viewDidLoad()
Свойство есть, в нем есть View, но вот в какой момент его добавить на экран? Логично, что в момент начала касания. Во ViewController нужно добавить метод touchesBegan(::)
Также выбираем из множества касаний первое и работе с этим касанием
В метод touchesMoved(::) в ветке, где обрабатываются проценты и вес в граммах нужно добавьте строку:
Тут мы задаем матрицу трансформации для увеличения размеров cicrcleView по высоте и ширине. Значения которые я передал в эту матрицу — это результат подбора наиболее удобных значений. Подбором методом «тыка». Так что можете поэкспериментировать и выбрать значения, которые удобны вам.
Ну и наконец при завершении касаний нужно отменить трансформацию для cicrleView и убрать его с экрана. У нас уже есть метод, где можно это отработать. В метод touchesEnded(::) добавьте две строки:
Полностью код ViewController выглядит так:
Код файла ScaleView:
Ссылка проекта на GitHub
Как я уже сказал, основной функционал готов, остались только доработки:
- При достижении максимального значения я хочу чтобы срабатывал виброотклик как в статье Haptic feedback на iPhone 6s
- Обновление значений в UILabel происходят очень быстро, (я думаю вы это заметили при тестировании) поэтому нужно добавить некую плавность.
- В месте нажатия должен появляться полупрозрачный круг. Его диаметр должен увеличиваться по мере увеличения силы нажатия и уменьшаться по мере уменьшения силы нажатия
Два первых пункта я не смогу отобразить ни с помощью скриншотов, ни с помощью анимированных gif. Поэтому сделайте дополнения, описанные ниже и проверяете самостоятельно на своих устройствах. А вот третий пункт я продемонстрирую. Но давайте дополнения сделаем все вместе.
Виброотклик Haptic feedback
Импортируем фреймворк AudioToolbox перед декларацией класса ViewController, а также добавим свойство isPlaySound для исключения многократного проигрывания виброотклика.
import UIKit
import AudioToolbox
class ViewController: UIViewController {
@IBOutlet weak var scaleView: ScaleView!
@IBOutlet weak var forceLabel: UILabel!
@IBOutlet weak var grammLabel: UILabel!
var isPlaySound = true
...
Далее внесите изменения в метод touchesMoved(::), чтобы он выглядел следующим образом:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
cicrcleView.center = touch.location(in: view)
if #available(iOS 9.0, *) {
if traitCollection.forceTouchCapability == UIForceTouchCapability.available {
if touch.force >= touch.maximumPossibleForce {
forceLabel.text = "100%+ force"
grammLabel.text = "385 грамм"
if isPlaySound { // Добавления // 1
AudioServicesPlaySystemSound(1519)
isPlaySound = false // 2
}
} else {
let force = (touch.force / touch.maximumPossibleForce) * 100
let grams = force * 385 / 100
let roundGrams = Int(grams)
isPlaySound = true // Добавления // 3
forceLabel.text = "\(Int(force))% force"
grammLabel.text = "\(roundGrams) грамм"
}
}
}
}
}
Ничего сложного — при запуске iOS приложения, а в данном случае при инициализации ViewController и создании свойства класса isPlaySound мы включаем возможность проигрывания звуков — в том числе и виброоткликов. При достижении максимальной силы нажатия происходит проверка isPlaySound (1) и если он true то выполняется вибрация и сразу срабатывает запрет (2) на проигрывание вибрации. Этот запрет снимается (3), если сила нажатия становится меньше максимально возможного значения.
Плавность обновления
Теперь про плавность. Обновления надписей присходит очень быстро, со скоростью срабатывания метода touchesMoved(::), а это несколько сотен срабатываний в секунду. Для уменьшения частоты обновления надписи я добавил свойство класса ViewController isUpdate и поставил наблюдателей свойств didSet.
var isUpdate = true {
didSet {
if isUpdate == false {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.isUpdate = oldValue
}
}
}
}
Суть подобной конструкции в том, что как только мы устанавливаем данное свойство в значение false оно возвращается в значение true через 0,01 секунды. Соответственно при записи текста в UILabel мы будем устанавливать значение свойства isUpdate в значение false и не позволим обновлять надписи пока оно не станет true. Поэтому обновления записей будет происходить у нас не чаще одного раза в сотую доли секунды.
В методе touchesMoved(::) в ветке, где мы выводим показания % силы и веса в граммах измените код следующим образом:
if isUpdate {
forceLabel.text = "\(Int(force))% force"
grammLabel.text = "\(roundGrams) грамм"
isUpdate = false
}
Этого будет достаточно для придания плавности при обновлении надписей
Визуализация касания
Сначала давайте создадим UIView и сделаем его полупрозрачным и круглой формы. Для этого добавим свойство класса ViewController и сделаем первоначальные настройки в методе viewDidLoad()
let cicrcleView = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80)) // View 80 на 80
override func viewDidLoad() {
super.viewDidLoad()
forceLabel.text = "0% force"
grammLabel.text = "0 грамм"
cicrcleView.layer.cornerRadius = 40 // Закруглили углы по половине ширины View - получлся круг
cicrcleView.alpha = 0.6 // Прозрачность 60%
cicrcleView.backgroundColor = UIColor.red
}
Свойство есть, в нем есть View, но вот в какой момент его добавить на экран? Логично, что в момент начала касания. Во ViewController нужно добавить метод touchesBegan(::)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
cicrcleView.center = touch.location(in: view) // 1
view.addSubview(cicrcleView) // 2
}
}
Также выбираем из множества касаний первое и работе с этим касанием
- Устанавливаем координаты центра circleView в точку касания
- Добавляем cicrcleView на экран
В метод touchesMoved(::) в ветке, где обрабатываются проценты и вес в граммах нужно добавьте строку:
cicrcleView.transform = CGAffineTransform.init(scaleX: CGFloat(1 + (grams / 5) / 20), y: CGFloat(1 + (grams / 5) / 20))
Тут мы задаем матрицу трансформации для увеличения размеров cicrcleView по высоте и ширине. Значения которые я передал в эту матрицу — это результат подбора наиболее удобных значений. Подбором методом «тыка». Так что можете поэкспериментировать и выбрать значения, которые удобны вам.
Ну и наконец при завершении касаний нужно отменить трансформацию для cicrleView и убрать его с экрана. У нас уже есть метод, где можно это отработать. В метод touchesEnded(::) добавьте две строки:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
forceLabel.text = "0% force"
grammLabel.text = "0 грамм"
// Добавить
cicrcleView.removeFromSuperview() // Убрали круг
cicrcleView.transform = .identity // Убрали трансформацию для круга
}
Полностью код ViewController выглядит так:
import UIKit
import AudioToolbox
class ViewController: UIViewController {
@IBOutlet weak var scaleView: ScaleView!
@IBOutlet weak var forceLabel: UILabel!
@IBOutlet weak var grammLabel: UILabel!
var isPlaySound = true
var isUpdate = true {
didSet {
if isUpdate == false {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.isUpdate = oldValue
}
}
}
}
let cicrcleView = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
override func viewDidLoad() {
super.viewDidLoad()
forceLabel.text = "0% force"
grammLabel.text = "0 грамм"
cicrcleView.layer.cornerRadius = 40
cicrcleView.alpha = 0.6
cicrcleView.backgroundColor = UIColor.red
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
cicrcleView.center = touch.location(in: view) // 1
view.addSubview(cicrcleView) // 2
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
cicrcleView.center = touch.location(in: view)
if #available(iOS 9.0, *) {
if traitCollection.forceTouchCapability == UIForceTouchCapability.available {
if touch.force >= touch.maximumPossibleForce {
forceLabel.text = "100%+ force"
grammLabel.text = "385 грамм"
if isPlaySound {
AudioServicesPlaySystemSound(1519)
isPlaySound = false
}
} else {
let force = (touch.force / touch.maximumPossibleForce) * 100
let grams = force * 385 / 100
let roundGrams = Int(grams)
isPlaySound = true
if isUpdate {
forceLabel.text = "\(Int(force))% force"
grammLabel.text = "\(roundGrams) грамм"
isUpdate = false
}
cicrcleView.transform = CGAffineTransform.init(scaleX: CGFloat(1 + (grams / 5) / 20), y: CGFloat(1 + (grams / 5) / 20))
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
forceLabel.text = "0% force"
grammLabel.text = "0 грамм"
cicrcleView.removeFromSuperview()
cicrcleView.transform = .identity
}
}
Код файла ScaleView:
import UIKit
@IBDesignable
class ScaleView: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setStrokeColor(UIColor.red.cgColor)
context?.setLineWidth(14.0)
context?.addArc(center: CGPoint(x: 375 / 2, y: 375 / 2), radius: 375 / 2 - 14, startAngle: 0, endAngle: 2 * CGFloat(M_PI), clockwise: true)
context?.strokePath()
context?.setLineWidth(1.0)
context?.setStrokeColor(UIColor.lightGray.cgColor)
context?.addArc(center: CGPoint(x: 375 / 2, y: 375 / 2), radius: 375 / 4 - 14, startAngle: 0, endAngle: 2 * CGFloat(M_PI), clockwise: true)
context?.strokePath()
}
}
Ссылка проекта на GitHub