[UIKit] 텍스트필드 입력(클릭) 시 키보드 올리기/내리기 (2): Snpakit
🧸 시작
이전 글에서는 스토리보드를 활용해 키보드가 올라오거나 내려갈 때마다 제약조건을 변경해 UI를 조정하는 방법을 사용했다. 이번 글에서는 코드 기반의 접근 방법으로 동일한 작업을 구현하려 한다.
Snapkit 라이브러리를 이용하여 코드 기반 레이아웃 설정을 중심으로 다룰 것이다. 키보드가 화면을 가리는 문제를 해결하고 레이아웃을 조정하는 방법에 대해 설명할 것이다. 이 과정에서 NotificationCenter 을 사용한 키보드 알림 처리와 Snapkit을 사용하면서 유용한 방법을 소개할 것이다.
🧸 UI 구현
먼저 스토리보드를 사용하지 않고 코드베이스로 UI를 구현하기 위해서 세팅을 해준다. 처음 세팅 방법은 아래 글을 참조하면 된다.
[UIKit] 스토리보드 없는 프로젝트 세팅
프로젝트 생성 Main Interface 부분 삭제 version 14.2 Main 스토리보드 삭제 Info.plist 파일에서 스토리보드 삭제 메인 VC 설정 SceneDelegate 파일에 가서 설정한다 func scene(_ scene: UIScene, willConnectTo session: UISce
nlestory.tistory.com
이제 메인스토리보드를 지웠다면 이제 코드베이스를 가지고 UI를 만들어준다. 간단하게 텍스트필드와 버튼하나만 만들어 보겠다요~
빠르게 UI를 구성하기 위해서 Snpakit과 Then을 사용해보겠다. 아래 해당 라이브러리 깃허브 주소를 SPM을 사용하여 간편하게 설치할 수 있다.
import UIKit
import SnapKit
import Then
final class ViewController: UIViewController {
private let textField = UITextField().then {
$0.placeholder = "텍스트를 입력하세요"
$0.backgroundColor = .systemGray6
}
private lazy var button = UIButton().then {
$0.setTitle("버튼", for: .normal)
$0.backgroundColor = .tintColor
$0.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "키보드 테스트"
self.view.backgroundColor = .systemBackground
cofigureLayut()
}
private func cofigureLayut() {
self.view.addSubview(textField)
self.view.addSubview(button)
textField.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview().inset(20)
$0.height.equalTo(50)
}
button.snp.makeConstraints {
$0.top.equalTo(textField.snp.bottom).offset(50)
$0.width.equalTo(120)
$0.height.equalTo(40)
$0.centerX.equalToSuperview()
}
}
@objc private func buttonDidTap(_ sender: UIButton) {
print("버튼클릭")
view.endEditing(true)
}
}
![]() |
![]() |
간단하게 텍스트필드와 버튼으로 이루어진 화면을 만들었다. 텍스트필드를 클릭하면 키보드가 올라올 것이고 버튼을 클릭하면 키보드가 내려가게 된다.
오른쪽 화면에서 키보드가 올라갔을 때 버튼이 가려지는 것을 이제 해결해보겠다요.
얼만큼 올릴지를 생각하다가 텍스트필드와 버튼이 합쳐진 영역이 화면의 정가운데에 배치하는 것을 목표로 수정을 해야겠다.
레츠 고고
🧸 NotificationCenter
일단 키보드가 올라갈 때와 내려갈 때의 시점을 알아야하기 때문에 NotificationCenter를 사용하여 키보드를 표시하고 숨기는 알림을 받는다. 이 방법은 이 전의 글과 동일하다.
이전 글을 스토리보드로 진행했지만 코드베이스도 다를 것이 없다.
extension ViewController {
private func configureKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
@objc private func keyboardWillShow(_ notification: Notification) {
print("✍️ 키보드 나타나기~")
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
let keyboardHeight = keyboardFrame.height
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
print("👋 키보드 사라지기~")
}
}
위의 코드와 분리시키기 위해 extension으로 따로 작성하였다.
🧸 UI 업데이트
이제 키보드가 올라가고 사라질 때의 시점을 알게 되었고 키보드의 높이도 알게 되었으니 내가 할일은 제약조건을 수정시켜주는 일이다.
나타날 때 원하는 만큼 올려주고 사라질 때는 원상복구를 시키는 작업을 할 것이다.
내가 원하는 만큼은 키보드를 제외한 나머지 화면의 가운데이다.
그렇다면 일단 텍스트필드와 버튼, 두가지의 컴포넌트를 하나로 묶어주는 하나의 커다란 뷰가 필요하다.
private func configureLayut() {
self.view.addSubview(containerView)
containerView.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview()
}
containerView.addSubview(textField)
containerView.addSubview(button)
textField.snp.makeConstraints {
$0.top.equalToSuperview().inset(50)
$0.leading.trailing.equalToSuperview().inset(20)
$0.height.equalTo(50)
}
button.snp.makeConstraints {
$0.top.equalTo(textField.snp.bottom).offset(50)
$0.width.equalTo(120)
$0.height.equalTo(40)
$0.centerX.equalToSuperview()
$0.bottom.equalToSuperview().inset(50)
}
}
커다란 containerView 안에 텍스트필드와 버튼을 추가하였다. 이제 containerView의 제약 조건을 업데이트해주면 하위뷰들은 자동으로 움직이게 된다.
@objc private func keyboardWillShow(_ notification: Notification) {
print("✍️ 키보드 나타나기~")
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
let keyboardHeight = keyboardFrame.height
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview().offset(-(keyboardHeight / 2))
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
print("👋 키보드 사라지기~")
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview()
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
키보드가 나타날 때 해당 뷰를 원하는 높이만큼 제약조건을 수정하였다.
✔️ updateConstraints
스냅킷 라이브러리에는 updateConstraints를 이용하여 제약조건을 쉽게 업데이트할 수 있다. updateConstraints를 사용할 때 조심해야하는 점은 초기의 제약조건을 설정한 후에 해줘야한다는 점이다. 똑같은 제약조건에서 업데이트하는 것이기때문에 만약에 내가 업데이트할 제약조건이 top 이라면 초기에 makeConstraints를 할 때 top 제약조건이 포함되어있어야 한다.
containerView.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview()
}
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview().offset(-(keyboardHeight / 2))
}
✔️ remakeConstraints
여기에서 확인할 수 있는 것은 make 할 때 centerY라는 제약조건을 설정한 후의 동일한 제약조건을 수정한다는 점이다. 하지만 제약조건이 변경되는 것이 많아 어떻게 설정되어있는 지 헷갈리거나 알 수 없다면 remakeConstraints를 사용하는 것도 방법이다.
containerView.snp.remakeConstraints {
$0.centerY.equalToSuperview().offset(-(keyboardHeight / 2))
$0.leading.trailing.equalToSuperview()
}
만약 remake를 사용한다면 변경할 제약조건뿐 아니라 모든 제약조건을 다시 재설정해줘야한다.
remake를 확인해보면 모든 제약조건을 제거한 후에 다시 설정해주기 때문에 잘 설정해줘야한다!
여기에서 중요한 것은 Inset이 아니라 Offset를 사용한다는 점이다. 가끔 -라는 값을 적는 것을 선호하지 않는 점때문에 Inset을 사용하는 것을 많이 봤었다. 하지만 여기서 Inset을 사용하게 되면 레이아웃이 변하지 않는다. 나도 이거때문에 한참 수정하는 데 시간이 들었다. 다들 이런 거 때문에 시간낭비하지 않길.. (다음 번에는 이 inset과 Offset의 차이점을 알아봐야겠다)
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview().offset(-(keyboardHeight / 2))
}
✔️ 애니메이션
그리고 나서 애니메이션 속도를 주면서 화면을 업데이트하게 했다.
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
![]() |
![]() |
애니메이션 추가 X | 애니메이션 추가 O |
📂 소스코드 정리
import UIKit
import SnapKit
import Then
final class ViewController: UIViewController {
private let containerView = UIView().then {
$0.backgroundColor = .lightGray
}
private let textField = UITextField().then {
$0.placeholder = "텍스트를 입력하세요"
$0.backgroundColor = .systemGray6
}
private lazy var button = UIButton().then {
$0.setTitle("버튼", for: .normal)
$0.backgroundColor = .tintColor
$0.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "키보드 테스트"
self.view.backgroundColor = .systemBackground
configureLayut()
configureKeyboardNotifications()
}
private func configureLayut() {
self.view.addSubview(containerView)
containerView.snp.remakeConstraints {
$0.centerY.equalToSuperview().offset(100)
$0.leading.trailing.equalToSuperview()
}
containerView.addSubview(textField)
containerView.addSubview(button)
textField.snp.makeConstraints {
$0.top.equalToSuperview().inset(50)
$0.leading.trailing.equalToSuperview().inset(20)
$0.height.equalTo(50)
}
button.snp.makeConstraints {
$0.top.equalTo(textField.snp.bottom).offset(50)
$0.width.equalTo(120)
$0.height.equalTo(40)
$0.centerX.equalToSuperview()
$0.bottom.equalToSuperview().inset(50)
}
}
@objc private func buttonDidTap(_ sender: UIButton) {
print("버튼클릭")
view.endEditing(true)
}
}
extension ViewController {
private func configureKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
@objc private func keyboardWillShow(_ notification: Notification) {
print("✍️ 키보드 나타나기~")
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
let keyboardHeight = keyboardFrame.height
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview().offset(-(keyboardHeight / 2))
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
print("👋 키보드 사라지기~")
containerView.snp.updateConstraints {
$0.centerY.equalToSuperview()
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}