3DTouch – Весы на iPhone: Завершение

  • Tutorial
В прошлой статье мы разобрали работу с 3DTouch на iPhone. Теперь осталось завершить пару штрихов и закончить наше приложение.

image

Как я уже сказал, основной функционал готов, остались только доработки:

  1. При достижении максимального значения я хочу чтобы срабатывал виброотклик как в статье Haptic feedback на iPhone 6s
  2. Обновление значений в UILabel происходят очень быстро, (я думаю вы это заметили при тестировании) поэтому нужно добавить некую плавность.
  3. В месте нажатия должен появляться полупрозрачный круг. Его диаметр должен увеличиваться по мере увеличения силы нажатия и уменьшаться по мере уменьшения силы нажатия

Два первых пункта я не смогу отобразить ни с помощью скриншотов, ни с помощью анимированных 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
        }
    }

Также выбираем из множества касаний первое и работе с этим касанием

  1. Устанавливаем координаты центра circleView в точку касания
  2. Добавляем 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
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 1
    0
    isUpdate — очень корявое решение, вы просто при некоторых условиях пропускаете обновление лейбл, существует вероятность что последнее обновление вы как раз и пропустите, тем самым отобразив не верные (не финальные) результаты. лучше будет если вы будете использовать очереди, отложенные вызовы или хотябы таймер. например сохраняя значение в переменную класса, и при каждом срабатывании инитить таймер класса если его нет, а при срабатывании таймера удалять/обнулять его в классе и обновлять лейбу из переменной класса…

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое