Привет Хабр! В этой статье я покажу, как можно создать в 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
    02.08.2018 10:57
    +1

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


  1. asklv Автор
    02.08.2018 11:28

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


    1. peresada
      02.08.2018 11:31

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


      1. asklv Автор
        02.08.2018 11:37

        Css в плане кнопок — более продвинутый инструмент.


        1. swiftmind
          02.08.2018 12:32

          Для такого рода анимации лучше использовать


          1. asklv Автор
            02.08.2018 12:56

            Что лучше использовать для такого рода анимации?


            1. swiftmind
              02.08.2018 23:41

              Не смог отредактировать коментарий, должен быть линк на developer.apple.com/documentation/quartzcore/cadisplaylink?changes=_8


  1. leon4uk
    02.08.2018 12:38
    +3

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


    1. asklv Автор
      02.08.2018 12:53
      -2

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



      1. leon4uk
        02.08.2018 13:26

        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)
                    })
                }
            }
        }


        1. asklv Автор
          02.08.2018 13:46

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


          1. leon4uk
            02.08.2018 13:50

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


            1. asklv Автор
              02.08.2018 14:01

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


              1. leon4uk
                02.08.2018 14:06

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

                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)
                            })
                        }
                    }
                }


                1. asklv Автор
                  02.08.2018 14:36

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


                  1. s_suhanov
                    02.08.2018 15:58
                    +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)
                            })
                        }
                    }

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


                    1. asklv Автор
                      02.08.2018 16:19
                      +1

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


                      1. s_suhanov
                        02.08.2018 16:36

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


                        1. asklv Автор
                          02.08.2018 16:44

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


                          1. leon4uk
                            02.08.2018 17:17

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

                            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)
                                        })
                                    }
                                }
                            }


                          1. s_suhanov
                            02.08.2018 18:38

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


  1. Flexoid1
    02.08.2018 13:02

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

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


    1. asklv Автор
      02.08.2018 13:15
      -2

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


      1. AlexIzh
        02.08.2018 14:02

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


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

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


        1. asklv Автор
          02.08.2018 14:57

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


  1. haritowa
    02.08.2018 13:47

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

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


  1. alexwillrock
    02.08.2018 13:47

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


    1. asklv Автор
      02.08.2018 13:55

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


  1. haritowa
    02.08.2018 13:56

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


  1. asklv Автор
    02.08.2018 14:24

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


  1. asklv Автор
    02.08.2018 14:31

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


  1. yolondie
    02.08.2018 16:04

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


    1. asklv Автор
      02.08.2018 16:09

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


    1. asklv Автор
      02.08.2018 16:51

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


      1. yolondie
        02.08.2018 18:38

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

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


  1. asklv Автор
    03.08.2018 10:08

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