Кастомная кнопка с простой анимацией на Swift. Пример 1 0



Привет Хабр! В этой статье я покажу, как можно создать в Xcode свою кастомную кнопку с простой анимацией с помощью языка Swift. Совместимость: iOS8 или выше.

Это просто пример, поэтому всевозможные права доступа, типа private и т. п., здесь не используются. Также подразумевается, что вы уже знакомы с основами Xcode и Swift и сможете без труда создать начальный «Single View App»?проект в среде Xcode, этот этап также будет опущен.

Кнопка будет выглядеть следующим образом:
imageНормальное состояние
imageНажатое состояние
imageАнимация

Подготовка


Добавляем в проект новый файл с незатейливым названием CustomButton1.swift, в котором будет находиться класс новой кнопки с таким же незатейливым названием, наследуемый от UIButton.

//
//  CustomButton1.swift
//

import UIKit

class CustomButton1: UIButton {
}

Далее в Main.storyboard добавляем кнопку в контроллер, присваиваем ей класс CustomButton1. Изменяем тип кнопки на Custom, а цвет Background на оранжевый.

image

Настройка кнопки


Кнопка будет иметь закруглённые края. Анимация будет осуществляться путём изменения альфа?канала заднего плана с 0.3 до 1. Альфа?канал названия кнопки при этом должен оставаться неизменным, поэтому для заднего цвета будет использоваться layer.backgroundColor, а не backgroundColor, который мы задали ранее.

Добавляем изменяемое поле для хранения цвета (по умолчанию чёрный), неизменяемое поле для хранения значения альфа?канала при нажатом состоянии и метод, который будет осуществлять настройку кнопки.

class CustomButton1: UIButton {

    var color: UIColor = .black
    let touchDownAlpha: CGFloat = 0.3

    func setup() {
        backgroundColor = .clear
        layer.backgroundColor = color.cgColor

        layer.cornerRadius = 6
        clipsToBounds = true
    }
}

Автовызов настройки кнопки


Для того, чтобы настройки к кнопке из Storyboard применились автоматически, нужно добавить setup в метод awakeFromNib, не забыв при этом сохранить значение backgroundColor перед тем, как его обесцветить.

override func awakeFromNib() {
    super.awakeFromNib()

    if let backgroundColor = backgroundColor {
        color = backgroundColor
    }

    setup()
}

Программное создание кнопки


Не всегда удобно использовать Storyboard для создания кнопки, иногда лучше это сделать из программы. Для этого пропишем вспомогательный инициализатор.

convenience init(color: UIColor? = nil, title: String? = nil) {
    self.init(type: .custom)

    if let color = color {
        self.color = color
    }

    if let title = title {
        setTitle(title, for: .normal)
    }

    setup()        
}

Теперь, чтобы создать такую же кнопку внутри программы, достаточно написать так.

let button = CustomButton1(color: .orange, title: "Button")

Настройка кнопки готова.

События нажатия и отпускания кнопки


Перед тем, как перейти к анимации, нужно как-то ловить события нажатия и отпускания кнопки. Это можно сделать через поле isHighlighted. Методы touchDown и touchUp будут описаны ниже, пока они пустые.

override var isHighlighted: Bool {
    didSet {
        if isHighlighted {
            touchDown()
        } else {
            cancelTracking(with: nil)
            touchUp()
        }
    }
}

func touchDown() {
}

func touchUp() {
}

Анимация


Самое интересное! Как же реализовать анимацию? Первым делом напрашивается UIView.animate, но здесь есть небольшая загвоздка. При нажатии кнопки альфа?канал заднего плана устанавливается в 0.3, а при отпускании плавно переходит в 1. Если в момент анимации пользователь снова нажал на кнопку, то анимация должна прерываться и значение альфа?канала снова должно быть 0.3. Напоминаю, что кнопка должна работать как в последней iOS, так и в iOS8. Я не нашел простого варианта, как можно прервать UIView.animate в iOS8, поэтому использовал для анимации простой таймер.

weak var timer: Timer?

func stopTimer() {
    timer?.invalidate()
}

deinit {
    stopTimer()
}

Заполнение метода touchDown. Нужно остановить анимацию, если она происходит, и установить альфа?канал заднего цвета в 0.3.

func touchDown() {
    stopTimer()

    layer.backgroundColor = color.withAlphaComponent(touchDownAlpha).cgColor
}

Заполнение метода touchUp. При отпускании кнопки нужно запустить анимацию, то есть сделать циклический таймер, который бы постепенно увеличивал альфа?канал заднего плана. Но какой шаг для таймера выбрать? Опытным путём я установил, что шаг в 10 миллисекунд одинаково хорошо работает на всех моделях iPhone/iPad, даже на старых — iPhone 4s/5. Если сделать шаг чаще, то на 32?битных контроллерах смотрится уже плохо.

Также опытным путём я установил, что стандартная анимация кнопок в iOS длится примерно 400 миллисекунд. Остаётся только вычислить альфа?шаг, который стоит прибавлять на каждом шаге таймера и написать метод, который бы осуществлял это прибавление и следил за окончанием анимации.

let timerStep: TimeInterval = 0.01
let animateTime: TimeInterval = 0.4
lazy var alphaStep: CGFloat = {
    return (1 - touchDownAlpha) / CGFloat(animateTime / timerStep)
}()

func touchUp() {
    timer = Timer.scheduledTimer(timeInterval: timerStep,
                                 target: self,
                                 selector: #selector(animation),
                                 userInfo: nil,
                                 repeats: true)
}

@objc func animation() {
    guard let backgroundAlpha = layer.backgroundColor?.alpha else {
        stopTimer()

        return
    }

    let newAlpha = backgroundAlpha + alphaStep

    if newAlpha < 1 {
        layer.backgroundColor = color.withAlphaComponent(newAlpha).cgColor
    } else {
        layer.backgroundColor = color.cgColor

        stopTimer()
    }
}

Вот собственно и всё. Кнопка готова. Спасибо за внимание!

> Проект на GitHub

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (37):

  1. peresada
    /#18949821 / +1

    Я может что-то не понимаю, но это нормально писать 200 строк кода для одной кнопки с элементарной анимацией?

  2. asklv
    /#18949979

    Это риторический вопрос? Я не знаю другого способа, как можно получить подобную кнопку в Xcode сразу из коробки. Наверное, есть для этого какие-то библиотеки, но я старался показать, как можно сделать подобную кнопку самому. Хотя я полностью согласен, что хотелось бы иметь в среде разработки какой-то набор простых кнопок, чтобы не создавать их самостоятельно.

    • peresada
      /#18949995

      Я не знаком со Swift и Xcode, поэтому и спросил, так как смотрю на это со стороны css например, где подобную кнопку можно оформить в 10-15 строк даже с адаптивом без плагинов, а если с плагинами, то и вообще ничего не придется практически писать.

  3. leon4uk
    /#18950297 / +3

    Вы после нескольких лет разработки под iOS делаете анимации таймером? Вы хоть документацию открывали хоть раз, вы знаете что есть документация?

    • asklv
      /#18950347 / -2

      «Болтовня ничего не стоит. Покажите мне код.» — Линус Торвальдс. Да, я открывал документацию. Да, после нескольких лет разработки под iOS, я считаю, что это самый простой способ достигнуть нужного результата для iOS8. Пожалуйста, покажите мне, как это можно сделать лучше.

      • leon4uk
        /#18950489

        class CustomButton: UIButton {
            override var isHighlighted: Bool {
                didSet {
                    guard let color = backgroundColor else { return }
        
                    layer.removeAllAnimations()
                    UIView.animate(withDuration: 0.4, delay: 0.0, options: [.allowUserInteraction], animations: {
                        self.backgroundColor = color.withAlphaComponent(self.isHighlighted ? 0.3 : 1)
                    })
                }
            }
        }

        • asklv
          /#18950567

          Создайте в Xcode проект и добавьте туда две кнопки. Одну как написал я, одну как предложили вы. Потом очень быстро пощёлкайте по ним мышкой, и вы сразу увидите, чем отличается ваша анимация от моей.

          • leon4uk
            /#18950583

            Это все нюансы, добавление `.beginFromCurrentState` решает это.

            • asklv
              /#18950623

              Всё также хотелось бы увидеть код.

              • leon4uk
                /#18950643

                Вы серьезно? Ну ладно, вот копипаста с доп опцией:

                class CustomButton: UIButton {
                    override var isHighlighted: Bool {
                        didSet {
                            guard let color = backgroundColor else { return }
                
                            layer.removeAllAnimations()
                            UIView.animate(withDuration: 0.4, delay: 0.0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
                                self.backgroundColor = color.withAlphaComponent(self.isHighlighted ? 0.3 : 1)
                            })
                        }
                    }
                }

                • asklv
                  /#18950801

                  Создайте в Xcode проект и добавьте туда ТРИ кнопки. Одну как написал я, ДВЕ как предложили вы. Потом очень быстро пощёлкайте по ним мышкой, и вы сразу увидите, чем отличается ваша анимация от моей. Ваш последний пример будет работать хуже всех.

                  • s_suhanov
                    /#18951195 / +1

                    Тут предлагают еще такой код:


                    class CustomButton: UIButton {
                        override func endTracking(_ touch: UITouch?,
                                                  with event: UIEvent?) {
                    
                            super .endTracking(touch, with: event)
                    
                            UIView.animateKeyframes(withDuration: 0.4,
                                                    delay: 0.0,
                                                    options: [.beginFromCurrentState,
                                                              .allowUserInteraction],
                                                    animations: {
                                self.backgroundColor = self.backgroundColor?.withAlphaComponent(self.isHighlighted ? 0.3 : 1)
                            })
                        }
                    }

                    Так работает достойно?

                    • asklv
                      /#18951301 / +1

                      Этот код работает прекрасно. Спасибо!

                      • s_suhanov
                        /#18951389

                        Автор реализации — Digimax, а не я.

                        • asklv
                          /#18951437

                          С разрешения автора я внесу его код в эту статью для читателей, указав его авторство.

                          • leon4uk
                            /#18951601

                            Вот еще вариант:

                            class CustomButton: UIButton {
                                override var isHighlighted: Bool {
                                    didSet {
                                        guard let color = backgroundColor else { return }
                            
                                        UIView.animate(withDuration: self.isHighlighted ? 0 : 0.4, delay: 0.0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
                                            self.backgroundColor = color.withAlphaComponent(self.isHighlighted ? 0.3 : 1)
                                        })
                                    }
                                }
                            }

                          • s_suhanov
                            /#18951905

                            Думаю, что он не будет против.

  4. Flexoid1
    /#18950389

    Альфа?канал названия кнопки при этом должен оставаться неизменным, поэтому для заднего цвета будет использоваться layer.backgroundColor, а не backgroundColor, который мы задали ранее.

    Что вы тут хотите сделать? И чем отличается layer.backgroundColor от backgroundColor?

    • asklv
      /#18950437 / -2

      Так выдаст ошибку: let backgroundAlpha = backgroundColor?.alpha
      А так нет: let backgroundAlpha = layer.backgroundColor?.alpha

      • AlexIzh
        /#18950631

        Вы конечно извините, но это совсем не отвечает на поставленный вопрос, а только показывает вашу некомпетентность и невежество в данном вопросе. Почему выдаст ошибку? Как это связано с "альфа-каналом названия" кнопки?
        Возможно вы хотели ответить подобным образом, но ошиблись в парочке слов:


        Так как, из CGColor легче получить альфа-канал, чем из UIColor(который backgroundColor), я буду обращаться к нему. Тут есть два способа его получить, мы можем получить его из UIColor (backgroundColor.cgColor) или из леера, который имеет тот же цвет (см. документацию как работает леер) — layer.backgroundColor. По каким-то личным причинам, я выбрал второй путь, и мой выбор никак не зависит от названия кнопки, потому что оба этих цвета — цвет фона кнопки. Извините, что ввел вас в заблуждение таким предложением, я обязательно его отредактирую, что бы не вводить в заблуждение следующих мои уважаемых читателей.

        При чем тут что выдаст ошибку — это только показывает ваше не знание данного вопроса.
        С уважением, читатель.

        • asklv
          /#18950921

          Статью отредактирую, внутри всё поясню.

  5. haritowa
    /#18950573

    Я правильно понимаю, что он завёл таймер, что анимировать по кадрам, но меняет свойства леера, который по-умолчанию анимируется?

    Спойлер
    Не пиши больше)0

  6. alexwillrock
    /#18950575

    не, писать анимацию через Таймер, когда есть UIView.animate — это фантастика)
    а не проще ли было например переопределить UIControlEvents методы?
    делается все тоже самое, только в 30 строк и на родных средствах анимирования без костылей

    • asklv
      /#18950597

      Если создать анимацию с помощью UIView.animate, а потом прервать её с помощью layer.removeAllAnimations(), то есть задержка. На iPhone 4s будет очень ощутимо.

  7. haritowa
    /#18950601

    Вообще я советую заменить таймер на современное API, в своей документации эпл советует всегда использовать более абстрактное и современное апи, если это возможно. Могу найти пруф, если интересно

  8. asklv
    /#18950737

    Отвечу всем сразу про анимацию, почему я выбрал именно это решение. Конечно же, анимацию надо делать через UIView.animate, так и написано в статье. Но там также написано, что её нужно прерывать, если пользователь ещё раз нажмёт на кнопку. Делается это через layer.removeAllAnimations(), но с большой задержкой. Это хорошо видно, если очень быстро нажать на кнопку несколько раз, поэтому данный способ не подходит. Вы может запустить код, предложенный leon4uk, и сравнить сами.

  9. asklv
    /#18950777

    Также вместо таймера можно использовать отдельный асинхронный поток asyncAfter, как предложил haritowa. Я так и выбирал между таймером и вторым потоком, но таймер мне показался более простым и надёжным инструментом. Это просто дело предпочтений.

  10. yolondie
    /#18951219

    А чем отличается эта анимация от той стандартной которая реализована в UIButton?
    Тем что она выполняется не 250 мс а 400 мс?

    • asklv
      /#18951249

      Я перепишу статью и постараюсь подробно объяснить, что и зачем я сделал. А откуда информация про 250 мс? Есть ссылка или что-то подобное?

    • asklv
      /#18951483

      Если сможете привести пример, как можно добиться подобной анимации через стандартные средства, то буду очень признателен.

      • yolondie
        /#18951907

        Создавайте кнопку со стилем .system и задавайте background image для состояния .normal.
        Анимация будет такая же как и сделали Вы.

        > А откуда информация про 250 мс? Есть ссылка или что-то подобное?
        В документации по Core Animation говориться, что длительность анимаций по умолчанию — 0.25 сек. Возможно для кнопки это и не так, но я предполагаю что длительность именно такая.

  11. asklv
    /#18953397

    Извините, решил в статье ничего не менять. Считаю публикацию на Хабре хорошим жизненным опытом, который не хотелось бы забывать. Код, приведённый в статье, полностью рабочий, а в комментариях есть примеры, как его улучшить. На этом всё. Спасибо.