애니메이션 클론코딩 스터디
in SOPT iOS
3주차는 ContentOffset 와 Skeleton Animation 에 대하여 공부하였다.
ContentOffset 이란 UIScrollView의 하위 프로퍼티중 하나다. 직관적으로 말하자면, ContentOffset 은 내가 스크롤 한 만큼의 좌표라고 생각할 수 있다. 3주차에서는 이를 이용하여 스크롤를 한계까지 늘렸을 때, 이미지가 늘어나 뷰의 백그라운드를 감추는 실습을 진행하였다. 그렇게 한 것이 더 에쁘다고 한다! 그렇다면 실습을 시작해보자 !
왼쪽은 익범 스장님이 보내주신 기존 코드, 오른쪽은 실습을 마친 뒤 완성 코드로 시뮬레이터를 돌려보았을 때 화면이다. 왼쪽처럼 어떠한 효과도 주지 않는다면 끝까지 당겼을 때 사진 크기가 변경되지 않고 당겨진다. 하지만 애니메이션을 추가하여 당겼을 때 확대되었다가 다시 돌아가는 화면을 구현하였다.
그럼 오늘 사용할 ContentOffset 이란 무엇일까?
직역하자면, content 의 시작점이 스크롤뷰의 시작점의 거리다... ? 예쁘게 포장해서 말해보면, ContentOffset 은 내가 스크롤 한 만큼의 좌표 라고 말할 수 있다. 따라서 스크롤 한 만큼의 거리가 얼마나 떨어져있는지를 계산하여 content 이미지가 늘어나게 되는 것이다. 위 시뮬레이터를 구현한 코드에서 두 가지 함수를 추가하면 완성되는 간단한(?) 애니메이션 이었다. 그럼 우선 익범스장님이 보내주신 코드부터 쪼개보자
- TableViewHeader class
총 3개의 파일을 주셨는데, 먼저 TableViewHeader class 는 header 에 있는 사진의 layout 을 재설정해주는 class 다. 아래는 TableViewHeader class 의 전체코드
class TableViewHeader: UITableViewHeaderFooterView {
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.setLayout()
}
internal func bindData(imagePath: String) {
guard let url = URL(string: imagePath) else {return}
self.imageView.kf.setImage(with: url)
}
private func setLayout() {
self.addSubview(imageView)
imageView.snp.makeConstraints {
$0.top.bottom.leading.trailing.equalToSuperview()
}
}
private let imageView = UIImageView().then {
$0.contentMode = .scaleAspectFill
}
}
쪼개보자!
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.setLayout()
}
required init 이란 init() 을 재정의해줘야 할때 required 를 앞에 붙여 사용한다. requried init?(coder: NSCoder) 와 같이 required init 과 (coder: NSCoder) 를 같이 써주는 경우가 많은데 이는 스토리보드나 Xib 파일을 이용해 UI 를 구성하여 이를 그대로 사용하는 것이 가능하게끔 해주는 함수로, 스토리보드를 사용하여 코드를 주셨기 때문에 위와 같이 작성하신 것 같다. 보통 UIView 를 상속받아서 생성자를 사용하려고 하면 requried init?(coder: NSCoder) 를 작성한다.
override init 이란 수퍼클래스와 같은 이름으로 초기화할 때 사용한다.
internal func bindData(imagePath: String) {
guard let url = URL(string: imagePath) else {return}
self.imageView.kf.setImage(with: url)
}
private, public 과 같이 internal 도 범위를 정하는 것인데, internal 이란 기본 접근 레벨이다. 하나의 모듈 내부에서만 접근 가능하며 내부 접근 수준의 level 이다. 위 함수는 image 를 묶어주는 bind 함수이다.
private func setLayout() {
self.addSubview(imageView)
imageView.snp.makeConstraints {
$0.top.bottom.leading.trailing.equalToSuperview()
}
}
private let imageView = UIImageView().then {
$0.contentMode = .scaleAspectFill
}
internal func setResizeView(_ yValue: CGFloat) {
self.imageView.snp.remakeConstraints {
$0.top.bottom.leading.trailing.equalToSuperview().inset(yValue)
}
}
위는 image 에 대한 layout 을 설정한 것이다. setResizeView 의 layout 을 다시 잡아주는 것이다. ⭐️이번 실습의 메인 코드⭐️
- CustomTVC class
두 번째는 CustomTVC 로 애니메이션을 구현하는 class 다. 우선 아래는 CustomTVC 의 전체코드
//
// CustomTVC.swift
// Stretch-Header-example
//
// Created by ikbum on 2023/05/10.
//
import UIKit
class CustomTVC: UITableViewCell {
static let identifier: String = "CustomTVC"
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style,
reuseIdentifier: reuseIdentifier)
self.backgroundColor = .white
self.setLayout()
}
private func setLayout() {
self.addSubview(label)
label.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview().inset(50)
$0.height.equalTo(30)
}
}
private func bindText() {
self.label.text = "안녕하세요!"
self.label.backgroundColor = .clear
}
private let label = UILabel().then {
$0.textColor = .blue
}
}
쪼개보자!
static let identifier: String = "CustomTVC"
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style,
reuseIdentifier: reuseIdentifier)
self.backgroundColor = .white
self.setLayout()
self.skeletonAnimate()
}
private func setLayout() {
self.addSubview(label)
label.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview().inset(50)
$0.height.equalTo(30)
}
}
위 코드는 처음 view 가 나타날 때 보이는 swift 사진과 아래 cell 들에 대한 layout 과 관련된 코드다.
private func bindText() {
self.label.text = "안녕하세요!"
self.label.backgroundColor = .clear
}
private let label = UILabel().then {
$0.textColor = .blue
}
bindText()와 label 은 는 각 cell 안에 넣고자 하는 text 다.
- ViewController class
ViewController 는 위에서 짠 class 들을 모아 view 를 뙇 하고 보여주고자 하는 class 다. 그럼 전체코드!
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.setLayout()
self.setTableViewConfig()
}
private func setLayout() {
self.view.addSubview(tableView)
tableView.snp.makeConstraints {
$0.top.leading.trailing.bottom.equalToSuperview()
}
}
private func setTableViewConfig() {
self.tableView.delegate = self
self.tableView.dataSource = self
let headerView = TableViewHeader()
headerView.bindData(imagePath: "https://www.hackingwithswift.com/uploads/swift-evolution-7.jpg")
self.tableView.tableHeaderView = headerView
self.tableView.register(CustomTVC.self,
forCellReuseIdentifier: CustomTVC.identifier)
self.tableView.tableHeaderView?.frame = .init(origin: .zero,
size: .init(width: UIScreen.main.bounds.width,
height: 300))
}
private let tableView = UITableView(frame: .zero, style: .plain).then {
$0.contentInsetAdjustmentBehavior = .never
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
if let headerView = self.tableView.tableHeaderView as? TableViewHeader {
headerView.setResizeView(scrollView.contentOffset.y)
}
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTVC.identifier) as? CustomTVC else {return UITableViewCell()}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
쪼개보자!
private func setTableViewConfig() {
self.tableView.delegate = self
self.tableView.dataSource = self
let headerView = TableViewHeader()
headerView.bindData(imagePath: "https://www.hackingwithswift.com/uploads/swift-evolution-7.jpg")
self.tableView.tableHeaderView = headerView
self.tableView.register(CustomTVC.self,
forCellReuseIdentifier: CustomTVC.identifier)
self.tableView.tableHeaderView?.frame = .init(origin: .zero,
size: .init(width: UIScreen.main.bounds.width,
height: 300))
}
tableViewCell 구현하는 코드! 지난 세미나에서 한 번 해보았는데 이렇게 다시 보니 반갑다. tableView 에 대해 delegate, dataSource 선언해주었고, register 하여 등록도 해주었다.
private let tableView = UITableView(frame: .zero, style: .plain).then {
$0.contentInsetAdjustmentBehavior = .never
}
}
tableView 선언하였는데 contentInsetAdjustmentBehavior 를 처음 보았다.
contentInsetAdjustmentBehavior 란 scrollview 의 content 영역에 safe area insets 를 어떤 방식으로 적용할건지 결정하는 프로퍼티다. scrollview 는 overlapping bars 를 고려하여 content inset 을 조정하는데, 이를 조절할 수 있는 옵션이 contentInsetAdjustmentBehavior 이다. 따라서 never 로 선언하게 되면 safe area inset 을 무시하게 된다. 쉽게 말하면, safe area 너머 content 가 시작되지않도록 safe area 안에서부터 시작되도록 하는 것이 contentInsetAdjustmentBehavior 이다.
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
if let headerView = self.tableView.tableHeaderView as? TableViewHeader {
headerView.setResizeView(scrollView.contentOffset.y)
}
}
}
}
contentOffset의 y 값이 0보다 작다면 ( = 헤더가 내려왔다면 = 사진을 늘렸다면 ) headerView 의 size 를 다시 설정해준다. 따라서 사진을 직접적으로 늘리지 않아도 실제로는 늘어난 것처럼 y 값을 조정해주는 것이다. ⭐️이번 실습의 메인 코드⭐️
아래는 내가 이해못하자 친절하게 설명해준 익범 오빠의 영상 ,,, 체고
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: CustomTVC.identifier) as? CustomTVC else {return UITableViewCell()}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
cell 등록해주는 함수 !
이번 실습에서 이미지를 늘려줄 때 사용하는 메인 코드는 아래 두 부분이다.
//CustomTVC class 내부
internal func setResizeView(_ yValue: CGFloat) {
self.imageView.snp.remakeConstraints {
$0.top.bottom.leading.trailing.equalToSuperview().inset(yValue)
}
}
//ViewController class 내부
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
if let headerView = self.tableView.tableHeaderView as? TableViewHeader {
headerView.setResizeView(scrollView.contentOffset.y)
}
}
}
Skeleton Animation
Skeleton Animation 이란 어떤 특정 컴포넌트 위치에 해당 내용이 로딩되고 있다면 명시적으로 보여주기 위해 사용된다. 잘 만들어진 오픈소스도 많다고 한다.
위처럼 안녕하세요 글씨가 올라오기 전에 회색박스로 미리 로딩되고 있음이 나타난다. 이를 코드로 작성해보자.
private func skeletonAnimate() {
self.label.backgroundColor = .gray
UIView.animateKeyframes(withDuration: 2, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0,
relativeDuration: 1/2) {
self.label.alpha = 0.4
}
UIView.addKeyframe(withRelativeStartTime: 1/2,
relativeDuration: 1) {
self.label.alpha = 1
}
} completion: { _ in
self.bindText()
}
}
animateKeyframes 와 addKeyframe 을 설정하여 시간에 대한 설정을 해주었다! animateKeyframes 는 애니메이션을 중첩하고자 할 때 사용한다. 예를 들어, 1번 애니메이션 후에 2번을 하고 싶다.. 이러면 이제 animateKeyframes 를 불러주면 되는 것! 반복하고자 하는 요소를 넣어주고 completion 으로 '여기있습니다~' 해주면 된다. 그렇다면 반복하고자 하는 애니메이션이 여러개면 completion 도 여러개를 써야겠지? 이때 addKeyframe 이 도움이 된다. 간단히 말하자면 시작점과 끝점을 지정하는 효과를 적용할 수 있다. UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1/2) 는 애니메이션 함수가 호출되고 0초 후에 해당 애니메이션이 0.5초 재생된다는 것이다. 이를 이용하면 completion 핸들러에 핸들러가 물리고 ,,, 라는 일이 발생하지 않는다고 한다.
따라서 위 코드는 시작부터 0.5초까지 label 의 alpha 가 0.4로, 그 뒤에 1초 동안 alpha 가 1로 바뀐다는 의미다. 애니메이션이 끝났다면 text 가 등장한다. 이를 이용하면 천천히 애니메이션이 뜨는 구현이 가능하다!
정말 정말 신기하네 ,,, 이걸로 과제해야겠다. 매력적이야
'32YB SOPT > 애니메이션 스터디' 카테고리의 다른 글
[SOPT] 애니메이션_4주차 (Bezier Path, CAnimations) (0) | 2023.06.21 |
---|---|
[SOPT] 애니메이션_2주차 (UIGestureRecognizer 중 UIPanGestureRecognizer) (0) | 2023.04.30 |