Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 리펙터링
- tableView
- 클린 코드
- ribs
- Refactoring
- uiscrollview
- collectionview
- UICollectionView
- uitableview
- 애니메이션
- Xcode
- 스위프트
- clean architecture
- SWIFT
- map
- 리펙토링
- MVVM
- combine
- UITextView
- 리팩토링
- Protocol
- HIG
- swift documentation
- Observable
- ios
- RxCocoa
- rxswift
- Clean Code
- swiftUI
- Human interface guide
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. 키보드 처리 - 키보드가 올라갈 때 스크롤 뷰를 올리는 UI 본문
1. 키보드 처리 - 키보드가 올라갈 때 뷰를 올리는 UI
2. 키보드 처리 - 키보드가 올라갈 때 스크롤 뷰를 올리는 UI
키보드가 올라갈 때 스크롤 뷰를 올리는 UI
구현 아이디어
- keyboard를 감싸는 투명 UIView, keyboard 바로 위쪽을 감싸는 투명 UIView를 준비
- 투명 UIView는 hitTest를 사용하여 pass through하게 구현 (PassThroughView 구현은 이전 포스팅 글 참고)
- 키보드 바로 위쪽을 감싸는 투명 UIView위에 UIScrollView + UIStackView를 삽입
- UIStackView에 UITextView, UIButton을 넣으면 버튼이 화면 하단으로 가지 않고 중간에 있을것이므로, UITextView와 UIButton 중간 여백을 넣어주는 spacerView까지 추가
- 키보드가 올라올 때 UIScrollView의 frame.height값이 작아지는데 스크롤 뷰의 스크롤 위치는 그대로이므로, 그 타이밍에 스크롤을 맨 아래로 내리는 작업이 필요 (= 스크롤 뷰의 콘텐츠도 위로 밀리는 효과를 주기 위함)
구현
- 사용한 라이브러리
pod 'SnapKit'
pod 'Then'
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxGesture'
- 상속보다는 유지보수에 용이한 프로토콜 형태인 KeyboardWrapperable 구현
- 해당 프로토콜을 채택하는 UIViewController에서 사용할 수 있도록 구현
- didChangeKeyboardHeight 값은 이 프로토콜을 사용하는 쪽에서 키보드의 위치가 변경될 때 받아서 스크롤뷰의 스크롤을 맨 래로 이동시키고, UITextView와 UIButton 중간에 있는 spacerView를 숨겨지거나 보여지게 하기 위함
protocol KeyboardWrapperable {
var keyboardWrapperView: PassThroughView { get }
var keyboardSafeAreaView: PassThroughView { get }
var disposeBag: DisposeBag { get }
var didChangeKeyboardHeight: ((CGFloat) -> Void)? { get }
func setupKeybaordWrapper()
}
- 이후 KeyboardWrapperable의 extension구현은 이전 포스팅 글에서 알아본 과정과 동일
(완성된 KeyboardWrapperable)
import UIKit
import SnapKit
import RxSwift
import RxCocoa
private struct AssociatedKeys {
static var isEnabled = "isEnabled"
}
protocol KeyboardWrapperable {
var keyboardWrapperView: PassThroughView { get }
var keyboardSafeAreaView: PassThroughView { get }
var disposeBag: DisposeBag { get }
var didChangeKeyboardHeight: ((CGFloat) -> Void)? { get }
func setupKeybaordWrapper()
}
extension KeyboardWrapperable where Self: UIViewController {
private var isEnabled: Bool {
get {
(objc_getAssociatedObject(self, &AssociatedKeys.isEnabled) as? Bool) ?? false
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.isEnabled, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func setupKeybaordWrapper() {
guard !isEnabled else { return }
isEnabled.toggle()
setupLayout()
observeKeyboardHeight()
}
private func setupLayout() {
view.addSubview(keyboardWrapperView)
view.addSubview(keyboardSafeAreaView)
keyboardWrapperView.snp.makeConstraints {
$0.leading.trailing.bottom.equalToSuperview()
$0.height.equalTo(0).priority(.high)
}
keyboardSafeAreaView.snp.makeConstraints {
$0.top.leading.trailing.equalToSuperview()
$0.bottom.equalTo(keyboardWrapperView.snp.top)
}
}
private func observeKeyboardHeight() {
Observable<(Bool, Notification)>
.merge(
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.map { notification in (true, notification) },
NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification)
.map { notification in (false, notification) }
)
.bind(with: self) { ss, tuple in
let (isKeyboardUp, notification) = tuple
let uesrInfo = notification.userInfo
guard let endFrame = uesrInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let endFrameMinY = endFrame.origin.y
let shownKeyboardHeight = isKeyboardUp ? endFrame.height : 0
ss.didChangeKeyboardHeight?(shownKeyboardHeight)
ss.keyboardWrapperView.snp.updateConstraints {
$0.height.equalTo(shownKeyboardHeight).priority(.high)
}
UIView.transition(
with: ss.keyboardWrapperView,
duration: 0.25,
options: .init(rawValue: 458752),
animations: ss.view.layoutIfNeeded
)
}
.disposed(by: disposeBag)
}
}
사용하는쪽
- KeyboardWrappable
- 가장 중요한 부분은 Metric으로 interSpacing 값 (UITextView와 UIButton 사이에 들어갈 spacerView의 높이가 될 값) 계산을 해놓는 것
import UIKit
import Then
import SnapKit
import RxSwift
class ViewController: UIViewController, KeyboardWrapperable {
private enum Policy {
static let countOfText = 700
}
private enum Metric {
static let scrollViewTopSpacing = 30.0
static let textViewHeight = UIScreen.main.bounds.height * 0.5
static let stackViewSpacing = 10.0
static let spacing = 30.0
static let buttonHeight = 80.0
static let bottomSpacing = 30.0
static var interSpacing: CGFloat {
let safeInset = UIApplication.shared.windows.first?.safeAreaInsets ?? .zero
return UIScreen.main.bounds.height - (scrollViewTopSpacing + textViewHeight + buttonHeight + bottomSpacing + safeInset.top + safeInset.bottom)
}
}
...
var keyboardWrapperView = PassThroughView()
var keyboardSafeAreaView = PassThroughView()
var didChangeKeyboardHeight: ((CGFloat) -> Void)?
...
}
- keyboardSafeView에 scrollView를 넣고 scrollView안에 stacView를 넣어서 구현
keyboardSafeAreaView.addSubview(scrollView)
scrollView.addSubview(stackView)
stackView.addArrangedSubview(textView)
stackView.addArrangedSubview(interSpacerView)
stackView.addArrangedSubview(button)
stackView.addArrangedSubview(bottomSpacerView)
- 레이아웃
scrollView.snp.makeConstraints {
$0.top.equalToSuperview().inset(Metric.scrollViewTopSpacing)
$0.leading.trailing.equalToSuperview()
$0.bottom.equalToSuperview()
}
stackView.snp.makeConstraints {
$0.top.equalToSuperview().priority(.medium)
$0.leading.trailing.width.equalToSuperview()
$0.bottom.equalToSuperview().priority(.high)
}
textView.snp.makeConstraints {
$0.leading.trailing.equalToSuperview().inset(30)
$0.height.equalTo(Metric.textViewHeight)
}
interSpacerView.snp.makeConstraints {
$0.height.equalTo(Metric.interSpacing)
}
button.snp.makeConstraints {
$0.leading.trailing.equalToSuperview()
$0.height.equalTo(Metric.buttonHeight)
}
bottomSpacerView.snp.makeConstraints {
$0.height.equalTo(Metric.bottomSpacing)
}
- KeyboardWrapperable에서 키보드의 위치가 변경될때마다 호출되는 didChangeKeyboardHeight 클로저에 원하는 작업을 입력
- interSpacerView의 hidden 처리 (키보드가 올라올때 interSpacerView를 안보이게끔 처리)
- scrollToBottom 처리 (키보드의 위치가 변경될때마다 스크롤을 맨 아래로 내려야, 스크롤 뷰도 같이 올라가거나 내려가는것처럼 보이기 때문)
didChangeKeyboardHeight = { [weak self] height in
guard let self else { return }
print(height)
interSpacerView.isHidden = height != 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
self.scrollToBottom()
})
}
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height)
scrollView.setContentOffset(bottomOffset, animated: true)
}
(완성)
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments