기어가더라도 제대로
[UIKit-기초] 텍스트 필드 포커스 이동함에 따라 스크롤뷰 높이 조절(ScrollView Height adjust, 스크롤뷰 높이 조절, UITextField Focus) 본문
UIKit 기초
[UIKit-기초] 텍스트 필드 포커스 이동함에 따라 스크롤뷰 높이 조절(ScrollView Height adjust, 스크롤뷰 높이 조절, UITextField Focus)
Damagucci-juice 2024. 9. 25. 22:37Why
- 네이버 밴드의 투표 기능중에 스크롤이 되는 선택지 필드를 구현하고 싶었다.
- 정말 디테일이 아름답다.
기술 스택
- 반응형을 위해 Combine
- UI 편의 선언을 위해 Snapkit, Then
What
- 스크롤뷰에 항목이 동적으로 추가되게끔 스택뷰를 선언하고 레이아웃 잡기
- 한 텍스트 필드에서 다음 텍스트 필드로 포커스를 옮기기
- 키보드 높이 이외의 부분만큼으로 스크롤뷰의 자동 높이 조절
How
1. 스크롤뷰에 항목이 동적으로 추가되게끔 스택뷰를 컨텐트뷰로 선언하고 레이아웃 잡기
- 스크롤뷰는 뷰의 세이프 에이리어에
- 컨텐트뷰는 스크롤뷰의 “컨텐츠가이드 레이아웃”에
- 너비는 스크롤뷰와 같이
- 높이는 뷰의 높이보다 크거나 같은데 우선순위는 낮게
class ViewController: UIViewController {
let scrollView = UIScrollView().then {
$0.backgroundColor = .systemGray6
}
let contentStackView = UIStackView().then {
$0.axis = .vertical
$0.distribution = .fill
$0.alignment = .fill
$0.spacing = 2
$0.backgroundColor = .brown
}
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
private func setupLayout() {
view.addSubview(scrollView)
scrollView.addSubview(contentStackView)
// 스크롤뷰는 뷰의 세이프 에이리어에
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
// 컨텐트뷰는 스크롤뷰의 “컨텐츠가이드 레이아웃”에
let scrollViewLayoutGuide = scrollView.contentLayoutGuide
contentStackView.snp.makeConstraints { make in
make.edges.equalTo(scrollViewLayoutGuide)
// 너비는 스크롤뷰와 같이
make.width.equalTo(scrollViewLayoutGuide)
// 높이는 뷰의 높이보다 크거나 같은데 우선순위는 낮게
make.height.greaterThanOrEqualTo(view).priority(.low)
}
// 3칸 기본 텍스트 필드 만들기
(1...3).forEach { index in
let textField = makeTextField(index)
contentStackView.addArrangedSubview(textField)
}
}
}
class RendarableTextField: UITextField {
let index: Int
private let height = 50.0
private(set) var returnDidOccur = PassthroughSubject<Int, Never>()
init(index: Int) {
self.index = index
super.init(frame: .zero)
// appearance
self.backgroundColor = .white
self.text = "\(index): "
// delegate
self.delegate = self
setupLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupLayout() {
self.snp.makeConstraints { make in
make.width.equalTo(UIScreen.main.bounds.width)
make.height.equalTo(height)
}
}
}
extension RendarableTextField: UITextFieldDelegate { }
2. 한 텍스트 필드에서 다음 텍스트 필드로 포커스를 옮기기
- 키보드가 내려갔다가 올라가지 않게하기 위해서 Return이 발생했을 때 Delegate로 이벤트 발생
- 마지막 텍스트필드가 리턴을 누른 경우 새로운 텍스트 필드를 추가
// MARK: - ViewController
private func makeTextField(_ index: Int) -> RendarableTextField {
let textField = RendarableTextField(index: index)
// binding
textField.returnDidOccur
.receive(on: DispatchQueue.main)
.sink { [weak self] index in
self?.renderFocusToNext(from: index)
}
.store(in: &cancellables)
return textField
}
private func renderFocusToNext(from index: Int) {
guard contentStackView.arrangedSubviews.count > index,
let nextTextField = contentStackView.arrangedSubviews[index] as? RendarableTextField
else {
addLastTextField(index)
return
}
nextTextField.becomeFirstResponder()
func addLastTextField(_ index: Int) {
let lastTextField = self.makeTextField(index + 1)
contentStackView.addArrangedSubview(lastTextField)
lastTextField.becomeFirstResponder()
}
}
3. 키보드 높이 이외의 부분만큼으로 스크롤뷰의 자동 높이 조절
전체 화면의 높이 = 키보드 영역의 높이 + 나머지 영역 높이
ContentStackView의 높이가 나머지 영역의 높이를 넘어설 때, 즉 컨텐츠스택뷰의 화면이 키보드 영역 밑으로 들어 갈 때 ScrollView의 높이 조절 메서드를 트리거
// ViewController.swift
private var convertedKeyboardFrameEnd: CGRect?
// Keyboard Height
@objc func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo else { return }
guard let screen = notification.object as? UIScreen,
let keyboardFrameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let fromeCoordinateSpace = screen.coordinateSpace
let toCoordinateSpace: UICoordinateSpace = view
self.convertedKeyboardFrameEnd = fromeCoordinateSpace.convert(keyboardFrameEnd, to: toCoordinateSpace)
print(screen.bounds.height)
}
private func handleAddTextField(_ vm: PollTextFieldViewModel) {
let newTextField = PollTextFieldView(vm: vm)
self.textViews.append(newTextField)
textFieldStackView.addArrangedSubview(newTextField)
revealLastTextField()
self.view.layoutIfNeeded()
func revealLastTextField() {
let statusBarHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
let navigationBarHeight = navigationController?.navigationBar.bounds.height ?? 0.0
let stackViewBottomOffset = textFieldStackView.frame.origin.y + textFieldStackView.frame.height + statusBarHeight + navigationBarHeight
if let convertedKeyboardFrameEnd, stackViewBottomOffset > convertedKeyboardFrameEnd.origin.y {
let targetHeight = stackViewBottomOffset - convertedKeyboardFrameEnd.origin.y
// ScrollView 높이 조절 메서드
scrollView.setContentOffset(.init(x: 0, y: targetHeight), animated: true)
}
}
}
최종 코드
'UIKit 기초' 카테고리의 다른 글
[UIKit-기초] Xib에서 코드로 UIView 파일을 불러오기 (0) | 2025.01.26 |
---|---|
[UIKit-기초] UITableView 상단 영역까지 꽉 채우기 (0) | 2025.01.25 |
[UIkit-기초] Floating Panel (2) | 2024.07.22 |
[iOS-UIKit] iOS 15.0 이후 navigation bar appearance, status bar (0) | 2023.01.12 |
[UIKit-기초] ScrollView, blur, trnaslationX, fade-in, fade-out (0) | 2022.12.13 |
Comments