Breadcrumbs
앱을 사용할 때 키보드에 의해 컨텐츠가 가려질 경우 사용자에게 답답함을 줄 수 있습니다. 배경을 터치했을 때 키보드가 숨겨지는 기능으로 충분할 수도 있겠지만, 키보드가 올라왔을 때도 사용자가 모든 컨텐츠를 볼 수 있도록 화면을 스크롤할 수 있는 옵션도 제공하는 편이 좋습니다. 키보드가 없을 땐 스크롤이 되지 않고, 키보드가 올라왔을 때 컨텐츠를 스크롤할 수 있도록 만드는 방법을 정리합니다.
먼저 기본 뷰에 스크롤뷰, 스크롤 뷰 위에 나머지 컨트롤들이 올라갈 컨테이너 뷰를 추가합니다.
ViewController에서 스크롤뷰를 하나 만들고, Safe Area에 채워지도록 설정합니다.
private let scrollView: UIScrollView = {
let view = UIScrollView()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
let safeArea = view.safeAreaLayoutGuide
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
}
스크롤뷰 위에 여러가지 뷰나 컨트롤들이 올라갈 컨테이너 뷰를 하나 추가합니다.
private let containerView: UIView = {
let view = UIView()
return view
}()
컨테이너 뷰를 스크롤 뷰에 채워지도록 설정합니다.
scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
세로 스크롤이 가능하도록 하기 위해서 컨테이너 뷰의 너비는 스크롤뷰와 같도록 설정하고, 높이는 같지만 우선순위는 required가 아니도록 설정합니다.
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
let heightAnchor = containerView.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
heightAnchor.priority = .defaultHigh
heightAnchor.isActive = true
텍스트를 입력할 수 있는 간단한 컨텐츠를 구현하기 위해 Text View를 3개 추가합니다.
private let textView1: UITextView = {
let view = UITextView()
return view
}()
private let textView2: UITextView = {
let view = UITextView()
return view
}()
private let textView3: UITextView = {
let view = UITextView()
return view
}()
텍스트뷰들에 대한 레이아웃을 설정하고 간단히 색을 추가합니다.
containerView.addSubview(textView1)
containerView.addSubview(textView2)
containerView.addSubview(textView3)
textView1.translatesAutoresizingMaskIntoConstraints = false
textView1.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16).isActive = true
textView1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16).isActive = true
textView1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16).isActive = true
textView1.backgroundColor = UIColor.FlatColor.Green.ChateauGreen
textView2.translatesAutoresizingMaskIntoConstraints = false
textView2.topAnchor.constraint(equalTo: textView1.bottomAnchor, constant: 16).isActive = true
textView2.leadingAnchor.constraint(equalTo: textView1.leadingAnchor).isActive = true
textView2.trailingAnchor.constraint(equalTo: textView1.trailingAnchor).isActive = true
textView2.backgroundColor = UIColor.FlatColor.Yellow.Turbo
textView3.translatesAutoresizingMaskIntoConstraints = false
textView3.topAnchor.constraint(equalTo: textView2.bottomAnchor, constant: 16).isActive = true
textView3.leadingAnchor.constraint(equalTo: textView1.leadingAnchor).isActive = true
textView3.trailingAnchor.constraint(equalTo: textView1.trailingAnchor).isActive = true
textView3.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
textView3.backgroundColor = UIColor.FlatColor.Blue.Mariner
textView1.heightAnchor.constraint(equalTo: textView2.heightAnchor).isActive = true
textView2.heightAnchor.constraint(equalTo: textView3.heightAnchor).isActive = true
텍스트뷰들의 높이는 모두 같게 설정하고, 컨테이너뷰에서 16포인트씩 떨어지도록 설정합니다.
지금까지의 결과는 다음과 같습니다.
앱에서 키보드가 보여지거나 숨겨질 때 발생되는 이벤트를 받기 위해서 NotificationCenter의 addObserver 메서드를 이용할 수 있습니다.
// Register Keyboard notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
keyboardWillShowNotification
과 keyboardWillHideNotification
이 발생하면 각각 keyboardWillShow
와 keyboardWillHide
를 호출하게 됩니다.
iOS 9.0 이후부터는 추가적으로 observer를 등록 해제하는 작업을 하지 않아도 됩니다. 타겟 버전이 그 아래라면, View Controller의 deinit 등에서 removeObserver를 이용하여 observer를 해제해 줘야 합니다. - developer.apple.com
keyboardWillShow 메서드에서는 userInfo
딕셔너리에서 특정 키값을 이용해서 키보드 사이즈 정보를 알아내고, 이를 이용하여 스크롤뷰 설정을 수정합니다.
@objc private func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
let contentInset = UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: keyboardFrame.size.height,
right: 0.0)
scrollView.contentInset = contentInset
scrollView.scrollIndicatorInsets = contentInset
}
userInfo
딕셔너리는 notification을 받는 객체가 사용할 수 있는 추가적인 정보를 포함하고 있습니다. 이 정보는 notification을 보내는 객체가 notification을 보낼 때, 받는 쪽에서 활용할 수 있는 객체를 이 user information 딕셔너리에 포함시켜 보내게 됩니다. 현재 키보드에 대한 notification을 받겠다고 했으므로 userInfo
딕셔너리에는 UIKeyboardFrameBeginUserInfoKey
, UIKeyboardFrameEndUserInfoKey
, UIKeyboardAnimationDurationUserInfoKey
, UIKeyboardAnimationCurveUserInfoKey
등을 이용해 가져올 수 있는 키보드에 대한 정보들이 들어 있습니다.
애플 문서에 따르면, 키보드의 위치 및 사이즈 정보를 알 수 있는 키에는 UIKeyboardFrameBeginUserInfoKey
와 UIKeyboardFrameEndUserInfoKey
가 있습니다. 앱을 사용할 때 텍스트를 입력할 수 있는 컨트롤을 터치하면 키보드가 올라오는데, 이 때 애니메이션이 사용됩니다. UIKeyboardFrameBeginUserInfoKey
는 애니메이션의 시작 시점에서의 키보드 위치와 사이즈를 나타내고, UIKeyboardFrameEndUserInfoKey
는 애니메이션이 끝나는 시점에서의 키보드의 위치와 사이즈를 나타냅니다. 애니메이션이 완료된 시점에서의 키보드를 기준으로 스크롤뷰를 조정해야 하므로 end frame을 이용해야 합니다.
UIKeyboardFrameBeginUserInfoKey
를 이용하면 애니메이션이 끝난 후의 키보드 높이보다 약간 작은 값이 나오며, 이 값을 이용할 경우 스크롤뷰의 인셋을 조정하여도 컨텐츠의 일부가 키보드에 가려질 수 있습니다.나머지 두개의 키인 UIKeyboardAnimationDurationUserInfoKey
, UIKeyboardAnimationCurveUserInfoKey
를 이용해 각각 키보드가 올라오거나 내려가는 애니메이션의 duration과, 애니메이션 커브에 대한 정보를 가져올 수 있으며, 이것들을 이용해 키보드 애니메이션과 싱크가 맞는 커스텀 애니메이션을 만들 수 있습니다.
키보드의 높이는 size
의 height
프로퍼티에서 가져올 수 있습니다. 이 값을 스크롤뷰의 bottom content inset으로 주었습니다. content inset은 스크롤뷰의 edge로부터 content view 사이에 추가 공간을 주는 것이며, positive 값일 경우 이 inset 공간이 스크롤뷰 안쪽으로 들어올 수 있습니다.
다음 그림처럼 content inset만큼 아래쪽 공간이 더 생겨나서 디바이스의 화면인 스크롤뷰 안으로 들어오고, 그 빈 공간에 키보드가 위치하게 됩니다. scroll indicator에도 마찬가지로 inset을 주었습니다.
추가적으로, 다음과 같이 현재 입력하려고 하는 Text View를 알아내어 화면에 잘 보이도록 스크롤을 적절히 조절할 수도 있습니다. 입력 시점의 first responder를 알아내는 방법은 여기를 참고하였습니다.
@objc private func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
scrollView.contentInset.bottom = keyboardFrame.size.height
let firstResponder = UIResponder.currentFirstResponder
if let textView = firstResponder as? UITextView {
scrollView.scrollRectToVisible(textView.frame, animated: true)
}
}
키보드가 사라질 땐 화면을 원래 상태로 복구해 놓아야 합니다.
@objc private func keyboardWillHide() {
let contentInset = UIEdgeInsets.zero
scrollView.contentInset = contentInset
scrollView.scrollIndicatorInsets = contentInset
}
이상 키보드 유무에 대응하여 컨텐츠를 동적으로 조절하는 스크롤뷰를 구현하는 방법에 대해 정리해보았습니다.
전체 소스를 이곳의 DynamicScroll 프로젝트에서 확인 가능합니다.
tags: iOS - swift - UIKit