iOS Dev 37: Animating Layers with Core Animation | Swift 5, XCode 13
private func rotateImage() {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 2.0
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
imageView.layer.add(animation, forKey: nil)
}
easeInEaseOut
효과가 적용됨으로써 완료가 된 이후 다시 시작할 때까지의 텀private func animateRadius() {
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.fromValue = 0
animation.toValue = 100
animation.duration = 2.0
animation.autoreverses = true
animation.repeatCount = .infinity
imageView.layer.add(animation, forKey: nil)
}
cornerRadius
로 이미지 뷰의 이미지 라디우스 값을 변경하는 애니메이션autoreverses
값이 참이므로 원상복구가 되는 구조private func shakeField(_ textField: UITextField) {
let animation = CAKeyframeAnimation(keyPath: "position.x")
animation.values = [0, 10, -10, 10, 0]
animation.keyTimes = [0, 0.08, 0.24, 0.415, 0.5]
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.isAdditive = true
textField.layer.add(animation, forKey: nil)
}
keyTimes
으로 지정, values
와 연관isAdditive
값이 참이므로 현재 주어진 텍스트 필드의 위치에 해당 x값이 추가되는 구조@objc private func didTapButton() {
guard validateEmail() else {
shakeField(emailField)
return
}
guard validatePassword() else {
shakeField(passwordField)
return
}
DispatchQueue.main.async { [weak self] in
self?.rotateImage()
self?.animateRadius()
}
}
import UIKit
class ViewController: UIViewController {
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
private let emailField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter Email"
textField.textColor = .label
textField.borderStyle = .roundedRect
return textField
}()
private let passwordField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter Password"
textField.textColor = .label
textField.borderStyle = .roundedRect
textField.textContentType = .newPassword
textField.isSecureTextEntry = true
return textField
}()
private lazy var textFieldStackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [emailField, passwordField])
stackView.axis = .vertical
stackView.spacing = 30
stackView.distribution = .equalSpacing
return stackView
}()
private let loginButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .systemBlue
config.baseForegroundColor = .white
config.title = "LOGIN"
button.configuration = config
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setUI()
downloadImage()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
imageView.frame = CGRect(x: (view.frame.size.width - view.safeAreaInsets.left - view.safeAreaInsets.right - 200) / 2, y: view.safeAreaInsets.top + 50, width: 200, height: 200)
imageView.layer.cornerRadius = 200 / 2
}
private func setUI() {
view.backgroundColor = .systemBackground
view.addSubview(imageView)
view.addSubview(textFieldStackView)
view.addSubview(loginButton)
textFieldStackView.translatesAutoresizingMaskIntoConstraints = false
let stackViewConstraints = [
textFieldStackView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 50),
textFieldStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
textFieldStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
]
NSLayoutConstraint.activate(stackViewConstraints)
loginButton.translatesAutoresizingMaskIntoConstraints = false
let loginButtonConstraints = [
loginButton.topAnchor.constraint(equalTo: textFieldStackView.bottomAnchor, constant: 30),
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loginButton.widthAnchor.constraint(equalToConstant: 80),
loginButton.heightAnchor.constraint(equalToConstant: 30)
]
NSLayoutConstraint.activate(loginButtonConstraints)
loginButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
}
private func downloadImage() {
guard let url = URL(string: "https://pbs.twimg.com/media/E8oxWb0WEAEZmKx?format=png&name=small") else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 400,
error == nil else { return }
DispatchQueue.main.async { [weak self] in
self?.imageView.image = UIImage(data: data)
}
}
.resume()
}
@objc private func didTapButton() {
guard validateEmail() else {
shakeField(emailField)
return
}
guard validatePassword() else {
shakeField(passwordField)
return
}
DispatchQueue.main.async { [weak self] in
self?.rotateImage()
self?.animateRadius()
}
}
private func shakeField(_ textField: UITextField) {
let animation = CAKeyframeAnimation(keyPath: "position.x")
animation.values = [0, 10, -10, 10, 0]
animation.keyTimes = [0, 0.08, 0.24, 0.415, 0.5]
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.isAdditive = true
textField.layer.add(animation, forKey: nil)
}
private func validateEmail() -> Bool {
guard
let text = emailField.text,
!text.isEmpty else { return false }
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,30}"
let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
return emailPred.evaluate(with: text)
}
private func validatePassword() -> Bool {
guard
let text = passwordField.text,
text.count >= 8 else { return false }
return true
}
private func rotateImage() {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 2.0
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
imageView.layer.add(animation, forKey: nil)
}
private func animateRadius() {
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.fromValue = 0
animation.toValue = 100
animation.duration = 2.0
animation.autoreverses = true
animation.repeatCount = .infinity
imageView.layer.add(animation, forKey: nil)
}
}