fluid interface effect

3 minute read Published: 2019-11-27

wwdc2018 세션중 하나인 Designing Fluid Interfaces 를 직접 구현한 opensource중 button 구현에 대해 정리

ios app중 계산기앱의 버튼과 같은 효과

//
//  Button.swift
//  Example
//
//  Created by kwanghyun.won on 2019/11/07.
//  Copyright © 2019 vingle. All rights reserved.
//

import UIKit

open class Button: UIButton {
    private struct Const {
        static let borderWidth: CGFloat = 1
        static let cornerRadius: CGFloat = 2
    }

    open override var intrinsicContentSize: CGSize {
        var origSize = super.intrinsicContentSize
        origSize.height = self.size == .large ? origSize.height + 4.0 : origSize.height + 2.0
        //dump(origSize)
        return origSize
    }

    open override func tintColorDidChange() {
        super.tintColorDidChange()
        update()
    }

    open var style: ButtonStyle = .flat {
        didSet {
            if style != oldValue {
                update()
            }
        }
    }

    open var size: ButtonSize = .large {
        didSet {
            if size != oldValue {
                update()
            }
        }
    }

    open override var isHighlighted: Bool {
        didSet {
            if isHighlighted != oldValue {
                update()
            }
        }
    }

    open override var isEnabled: Bool {
        didSet {
            if isEnabled != oldValue {
                update()
            }
        }
    }

    private var animator = UIViewPropertyAnimator()
    private let activationFeedBackGenerator = UIImpactFeedbackGenerator(style: .light)

    public init(style: ButtonStyle = .flat, size: ButtonSize = .large) {
        self.style = style
        self.size = size
        super.init(frame: .zero)
        initialize()
    }
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }

    open override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = Const.cornerRadius
        layer.borderWidth = style == .ghost ? Const.borderWidth : 0.0
    }

    func initialize() {
        update()
        NotificationCenter.default.addObserver(self, selector: #selector(didContentSizeCategoryDidChange),
                                               name: UIContentSizeCategory.didChangeNotification, object: nil)
        addTarget(self, action: #selector(didTouchDown), for: [.touchDown, .touchDragEnter])
        addTarget(self, action: #selector(didTouchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])
    }

    private func update() {
        setTitleColor(style.titleColor, for: .normal)
        setTitleColor(style.hightlightTitleColor, for: .highlighted)
        setTitleColor(style.disableTitleColor, for: .disabled)

        self.backgroundColor = isEnabled ? style.backgroundColor : style.disableBackgroundColor

        layer.borderColor = isEnabled ? style.titleColor.cgColor : style.disableTitleColor.cgColor

        titleLabel?.font = size.titleFont
        let lineHeight: CGFloat = (size == .large) ? 22.0 : 19.0
        titleLabel?.setLineHeight(lineHeight: lineHeight)

        contentEdgeInsets = size.contentEdgeInsets
    }

}

// MARK: - event
extension Button {
    @objc private func didContentSizeCategoryDidChange() {
        update()
    }

    @objc private func didTouchDown() {
        animator.stopAnimation(true)
        backgroundColor = style.hightlightBackgroundColor
        layer.borderColor = Colors.Button.borderHighlighted.cgColor
        activationFeedBackGenerator.impactOccurred()
    }

    @objc private func didTouchUp() {
        animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut, animations: {
            self.backgroundColor = self.style.backgroundColor
            self.layer.borderColor = Colors.Button.border.cgColor
        })
        animator.startAnimation()
    }
}

private var animator = UIViewPropertyAnimator()
///...

    @objc private func didTouchDown() {
        animator.stopAnimation(true)
        backgroundColor = style.hightlightBackgroundColor
        //..
    }

    @objc private func didTouchUp() {
        animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeOut, animations: {
            self.backgroundColor = self.style.backgroundColor
            self.layer.borderColor = Colors.Button.border.cgColor
        })
        animator.startAnimation()
    }

flashlight button 과 같은 확대,축소 애니메이션과 진동 feedback

위 버튼 구현 소스에는 확대,축소 애니메이션은 구현되어 있지 않다 

    private let activationFeedBackGenerator = UIImpactFeedbackGenerator(style: 
    .light)
    //...
    @objc private func didTouchDown() {
        activationFeedBackGenerator.impactOccurred()
    }
 private func animate() {
        let timingParameters = UISpringTimingParameters(damping: 0.4, response: 0.2)
        let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
        animator.addAnimations {
            self.transform = CGAffineTransform(scaleX: 1, y: 1)
            self.backgroundColor = self.isOn ? self.onColor : self.offColor
        }
        animator.isInterruptible = true
        animator.startAnimation()
    }