Это просто пример, поэтому всевозможные права доступа, типа private и т. п., здесь не используются. Также подразумевается, что вы уже знакомы с основами Xcode и Swift и сможете без труда создать начальный «Single View App»?проект в среде Xcode, этот этап также будет опущен.
Кнопка будет выглядеть следующим образом:
Нормальное состояние
Нажатое состояние
Анимация
Подготовка
Добавляем в проект новый файл с незатейливым названием CustomButton1.swift, в котором будет находиться класс новой кнопки с таким же незатейливым названием, наследуемый от UIButton.
//
// CustomButton1.swift
//
import UIKit
class CustomButton1: UIButton {
}
Далее в Main.storyboard добавляем кнопку в контроллер, присваиваем ей класс CustomButton1. Изменяем тип кнопки на Custom, а цвет Background на оранжевый.
Настройка кнопки
Кнопка будет иметь закруглённые края. Анимация будет осуществляться путём изменения альфа?канала заднего плана с 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)
asklv Автор
02.08.2018 11:28Это риторический вопрос? Я не знаю другого способа, как можно получить подобную кнопку в Xcode сразу из коробки. Наверное, есть для этого какие-то библиотеки, но я старался показать, как можно сделать подобную кнопку самому. Хотя я полностью согласен, что хотелось бы иметь в среде разработки какой-то набор простых кнопок, чтобы не создавать их самостоятельно.
peresada
02.08.2018 11:31Я не знаком со Swift и Xcode, поэтому и спросил, так как смотрю на это со стороны css например, где подобную кнопку можно оформить в 10-15 строк даже с адаптивом без плагинов, а если с плагинами, то и вообще ничего не придется практически писать.
asklv Автор
02.08.2018 11:37Css в плане кнопок — более продвинутый инструмент.
swiftmind
02.08.2018 12:32Для такого рода анимации лучше использовать
asklv Автор
02.08.2018 12:56Что лучше использовать для такого рода анимации?
swiftmind
02.08.2018 23:41Не смог отредактировать коментарий, должен быть линк на developer.apple.com/documentation/quartzcore/cadisplaylink?changes=_8
leon4uk
02.08.2018 12:38+3Вы после нескольких лет разработки под iOS делаете анимации таймером? Вы хоть документацию открывали хоть раз, вы знаете что есть документация?
asklv Автор
02.08.2018 12:53-2«Болтовня ничего не стоит. Покажите мне код.» — Линус Торвальдс. Да, я открывал документацию. Да, после нескольких лет разработки под iOS, я считаю, что это самый простой способ достигнуть нужного результата для iOS8. Пожалуйста, покажите мне, как это можно сделать лучше.
leon4uk
02.08.2018 13:26class 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 Автор
02.08.2018 13:46Создайте в Xcode проект и добавьте туда две кнопки. Одну как написал я, одну как предложили вы. Потом очень быстро пощёлкайте по ним мышкой, и вы сразу увидите, чем отличается ваша анимация от моей.
leon4uk
02.08.2018 13:50Это все нюансы, добавление `.beginFromCurrentState` решает это.
asklv Автор
02.08.2018 14:01Всё также хотелось бы увидеть код.
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) }) } } }
asklv Автор
02.08.2018 14:36Создайте в Xcode проект и добавьте туда ТРИ кнопки. Одну как написал я, ДВЕ как предложили вы. Потом очень быстро пощёлкайте по ним мышкой, и вы сразу увидите, чем отличается ваша анимация от моей. Ваш последний пример будет работать хуже всех.
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) }) } }
Так работает достойно?
asklv Автор
02.08.2018 16:19+1Этот код работает прекрасно. Спасибо!
s_suhanov
02.08.2018 16:36Автор реализации — Digimax, а не я.
asklv Автор
02.08.2018 16:44С разрешения автора я внесу его код в эту статью для читателей, указав его авторство.
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) }) } } }
Flexoid1
02.08.2018 13:02Альфа?канал названия кнопки при этом должен оставаться неизменным, поэтому для заднего цвета будет использоваться layer.backgroundColor, а не backgroundColor, который мы задали ранее.
Что вы тут хотите сделать? И чем отличается layer.backgroundColor от backgroundColor?asklv Автор
02.08.2018 13:15-2Так выдаст ошибку: let backgroundAlpha = backgroundColor?.alpha
А так нет: let backgroundAlpha = layer.backgroundColor?.alphaAlexIzh
02.08.2018 14:02Вы конечно извините, но это совсем не отвечает на поставленный вопрос, а только показывает вашу некомпетентность и невежество в данном вопросе. Почему выдаст ошибку? Как это связано с "альфа-каналом названия" кнопки?
Возможно вы хотели ответить подобным образом, но ошиблись в парочке слов:
Так как, из CGColor легче получить альфа-канал, чем из UIColor(который backgroundColor), я буду обращаться к нему. Тут есть два способа его получить, мы можем получить его из UIColor (backgroundColor.cgColor) или из леера, который имеет тот же цвет (см. документацию как работает леер) — layer.backgroundColor. По каким-то личным причинам, я выбрал второй путь, и мой выбор никак не зависит от названия кнопки, потому что оба этих цвета — цвет фона кнопки. Извините, что ввел вас в заблуждение таким предложением, я обязательно его отредактирую, что бы не вводить в заблуждение следующих мои уважаемых читателей.
При чем тут что выдаст ошибку — это только показывает ваше не знание данного вопроса.
С уважением, читатель.
haritowa
02.08.2018 13:47Я правильно понимаю, что он завёл таймер, что анимировать по кадрам, но меняет свойства леера, который по-умолчанию анимируется?
СпойлерНе пиши больше)0alexwillrock
02.08.2018 13:47не, писать анимацию через Таймер, когда есть UIView.animate — это фантастика)
а не проще ли было например переопределить UIControlEvents методы?
делается все тоже самое, только в 30 строк и на родных средствах анимирования без костылейasklv Автор
02.08.2018 13:55Если создать анимацию с помощью UIView.animate, а потом прервать её с помощью layer.removeAllAnimations(), то есть задержка. На iPhone 4s будет очень ощутимо.
haritowa
02.08.2018 13:56Вообще я советую заменить таймер на современное API, в своей документации эпл советует всегда использовать более абстрактное и современное апи, если это возможно. Могу найти пруф, если интересно
asklv Автор
02.08.2018 14:24Отвечу всем сразу про анимацию, почему я выбрал именно это решение. Конечно же, анимацию надо делать через UIView.animate, так и написано в статье. Но там также написано, что её нужно прерывать, если пользователь ещё раз нажмёт на кнопку. Делается это через layer.removeAllAnimations(), но с большой задержкой. Это хорошо видно, если очень быстро нажать на кнопку несколько раз, поэтому данный способ не подходит. Вы может запустить код, предложенный leon4uk, и сравнить сами.
asklv Автор
02.08.2018 14:31Также вместо таймера можно использовать отдельный асинхронный поток asyncAfter, как предложил haritowa. Я так и выбирал между таймером и вторым потоком, но таймер мне показался более простым и надёжным инструментом. Это просто дело предпочтений.
yolondie
02.08.2018 16:04А чем отличается эта анимация от той стандартной которая реализована в UIButton?
Тем что она выполняется не 250 мс а 400 мс?asklv Автор
02.08.2018 16:09Я перепишу статью и постараюсь подробно объяснить, что и зачем я сделал. А откуда информация про 250 мс? Есть ссылка или что-то подобное?
asklv Автор
02.08.2018 16:51Если сможете привести пример, как можно добиться подобной анимации через стандартные средства, то буду очень признателен.
yolondie
02.08.2018 18:38Создавайте кнопку со стилем .system и задавайте background image для состояния .normal.
Анимация будет такая же как и сделали Вы.
> А откуда информация про 250 мс? Есть ссылка или что-то подобное?
В документации по Core Animation говориться, что длительность анимаций по умолчанию — 0.25 сек. Возможно для кнопки это и не так, но я предполагаю что длительность именно такая.
asklv Автор
03.08.2018 10:08Извините, решил в статье ничего не менять. Считаю публикацию на Хабре хорошим жизненным опытом, который не хотелось бы забывать. Код, приведённый в статье, полностью рабочий, а в комментариях есть примеры, как его улучшить. На этом всё. Спасибо.
peresada
Я может что-то не понимаю, но это нормально писать 200 строк кода для одной кнопки с элементарной анимацией?