Swift: сахарный синтаксис анимации UIView

Потому что закрытие делает уродливые пары

Если вы еще не слышали, замыкания - отличный инструмент для использования в вашем коде Swift. Они первоклассные граждане, они могут стать трейлинг-закрывающими, если они находятся в конце API, а теперь они @noescape по умолчанию, что является огромной победой в борьбе с ссылочными циклами.

Но время от времени нам приходится работать с API, которые содержат более одного замыкания, что превращает эту прекрасную языковую функцию во что-то гораздо менее привлекательное. Я смотрю на тебя, UIView.

class func animate(withDuration duration: TimeInterval,            
    animations: @escaping () -> Void,          
    completion: ((Bool) -> Void)? = nil)

Трейлинг-закрытие

UIView.animate(withDuration: 0.3, animations: {
    // Animations
}) { finished in
    // Compeleted
}

Мы смешиваем обычные замыкания с конечными замыканиями, animations: по-прежнему имеет заголовок параметра, но completion: теряет заголовок параметра, что делает его конечным замыканием. Я также чувствую, что замыкающее закрытие кажется отключенным от API в этом типе контекста, но я предполагаю, что это из-за закрывающих круглых скобок API и внутреннего замыкания, за которым следуют открывающие круглые скобки:

}) { finished in // yuck

Примечание. Если вы не знаете, что такое конечное замыкание, у меня есть еще одна статья, в которой объясняется, что это такое и как их использовать. Swift: чит-коды синтаксиса

Отступ для удобочитаемости

Можно также возразить против отступа по умолчанию для замыканий анимации, потому что они оба находятся на том же уровне, что и объявление. В последнее время я пил kool-aid по функциональному программированию большое время, и одна вещь, которая мне очень нравится в написании функционального кода, - это то, как мы перечисляем последовательности команд в формате маркированного списка:

[0, 1, 2, 4, 5, 6]
    .sorted { $0 < $1 } 
    .map { $0 * 2 }
    .forEach { print($0) }

Почему API двойного закрытия не может действовать таким же образом?

Примечание. Если вы не понимаете синтаксис $0, у меня есть другая статья, в которой объясняется, что они означают и как их использовать. Swift: чит-коды синтаксиса

Заставить уродливого быть красивым

UIView.animate(withDuration: 0.3,
    animations: {
        // Animations
    },
    completion: { finished in
        // Compeleted
    })

Я решил воспользоваться синтаксисом функционального программирования и лучше использовать отступы, борясь с автозаполнением Xcode и заставляя себя создавать UIView API-интерфейсы анимации, подобные этому. На мой взгляд, он предоставляет код в гораздо более читаемом формате, чем предыдущий, но это также труд любви. Каждый раз, когда вы копируете и вставляете этот код, отступ всегда сбивается, но я полагаю, что это больше проблема Xcode, чем Swift, верно?

Проходящие закрытия

let animations = {
    // Animate
}
let completion = { (finished: Bool) in
    // Completion
}
UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)

В начале этого поста я упомянул, что замыкания - это первоклассные граждане Swift-topia, что означает, что мы можем назначать их переменным и передавать их по желанию. Каким бы действенным ни был этот код, я не верю, что он читается так же хорошо, как предыдущий пример, и я не уверен, что другие объекты могут получить доступ и использовать эти закрытия, когда они были предназначены для одной цели. Если бы меня вынудили поставить ультиматум, я бы все равно выбрал первое.

Решение

Как и большинство программистов, я заставил себя создать решение относительно приземленной проблемы, пообещав себе, что это
«сэкономит время в долгосрочной перспективе».

UIView.Animator(duration: 0.3)
    .animations { 
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()

Как видите, синтаксис и структура были вдохновлены тем, что я узнал при использовании API функционального программирования Swift. Мы обменяли API двойного закрытия на последовательность функций более высокого порядка, и теперь наш код читается намного лучше, и компилятор борется за за нас, когда мы пишем новые линии и копировать / вставлять старые.

«Это сэкономит время в долгосрочной перспективе!»

Аниматор

class Animator {
    typealias Animations = () -> Void
    typealias Completion = (Bool) -> Void
    private var animations: Animations
    private var completion: Completion?
    private let duration: TimeInterval
    init(duration: TimeInterval) {
        self.animations = {}
        self.completion = nil
        self.duration = duration
    }
...

Наш тип Animator довольно простой, у него три свойства: длительность, два закрытия, инициализатор и некоторые функции, о которых мы вскоре поговорим. Мы использовали пару typealias определений для предопределения сигнатуры наших замыканий, что не обязательно, но всегда является хорошей практикой для улучшения читаемости нашего кода и уменьшения количества ошибок, когда или если мы решаем изменить сигнатуру нашего закрытия после того, как мы Реализовал их в нескольких местах.

Свойства замыкания являются изменяемыми, потому что нам нужно где-то их хранить, и мы хотим, чтобы значения изменились после создания экземпляра, но они также являются частными, потому что мы хотим избежать внешних мутаций. completion не является обязательным, чтобы напоминать официальный UIView API, а animations - нет. В нашей реализации инициализатора мы также определили значения по умолчанию для свойств замыкания, чтобы компилятор не жаловался.

func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}
func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}

Реализации последовательности закрытия невероятно просты, все, что они делают, это принимают конкретный аргумент закрытия и устанавливают собственное соответствующее значение закрытия на то, которое было передано.

Вернувшийся

Замечательно то, что эти API возвращают экземпляр Self, что является настоящим волшебством. Поскольку мы возвращаем Self, мы можем создать API в стиле последовательности.

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

let numbers = 
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $1 } // Returns Array
    .map { $0 * 2 }     // Returns Array

Однако, если последняя функция в последовательности возвращает объект, он должен быть назначен чему-то, чтобы компилятор мог что-то с ним сделать, поэтому мы присвоили его константе numbers.

Если последняя функция возвращает Void, то нам не нужно назначать ее чему-либо, чтобы она выполнялась:

[0, 1, 2, 4, 5, 6]         // Returns Array
    .sorted { $0 < $1 }    // Returns Array
    .map { $0 * 2 }        // Returns Array
    .forEach { print($0) } // Returns Void

Анимация

func animate() {
    UIView.animate(withDuration: duration, 
        animations: animations, 
        completion: completion)
}

Как и многие другие мои идеи, все эти изящные вещи заканчиваются простой оболочкой для уже существующего API, но это неплохо, совсем нет. Я твердо верю, что Swift был создан таким образом, чтобы мы, мыслители, мастера и программисты, могли переосмыслить и переработать предоставленные нам инструменты.

Расширение UIView

extension UIView {
    class Animator { ...

Наконец, мы берем наш Animator класс и помещаем его в расширение UIView,, которое мы делаем по нескольким причинам. Во-первых, нам нужно пространство имен UIView, чтобы оно предоставляло контекст для API, который мы только что создали, а во-вторых, функциональность напрямую связана с UIView, что сделало бы бессмысленным иметь его как отдельный класс.

Параметры

UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])

Есть несколько вариантов на выбор при работе с API анимации, просто ознакомьтесь с документацией. Благодаря возможности значений по умолчанию в функциях и наследовании классов классы Animator, а также SpringAnimator теперь охватывают большинство типов API анимации, которые вы обычно используете.

Как всегда, я создал на GitHub площадку, чтобы вы могли ее проверить, а также Суть на случай, если вы не знакомы с Xcode.

Если вам нравится то, что вы прочитали сегодня, вы можете проверить другие мои статьи, или если вы планируете применить этот подход для своего собственного проекта, отправьте мне твит или подпишитесь на меня в Twitter, это действительно делает мой день.