我有一个带有多个VC的PageViewController
。这些VC有一个UITextField
,它成为viewDidAppear
上的firstResponder。一切都包裹在NavigationController
中。
- NavigationController
--- MainViewController
----- PageViewController
------- ViewController
--------- ScrollView
----------- UITextField
前进很好。但是倒退,我遇到了一个使我发疯的错误:键盘随机停留。
TextFields
位于滚动视图中,该视图可观察键盘并进行滚动以避免文本字段被键盘隐藏。
导航/滚动的工作方式如下:每个VC都有一个“下一步”按钮。要向后导航,导航栏中有一个“后退”按钮。
一些代码。 基本VC:
import UIKit
import SnapKit
import RxSwift
import CLCarRentalCore
class DigitalBookingViewController: DigitalBookingBaseViewController {
// MARK: Interface Properties
internal let scrollView = UIScrollView()
internal let stackView = UIStackView()
internal let spacingView = View()
internal let nextButton = Button()
// MARK: Internal Properties
internal var editingTextField: UITextField?
internal let keyboardObserver = KeyboardObserver()
internal var observer: NSObjectProtocol?
internal let disposeBag = DisposeBag()
internal lazy var user: DigitalBookingUser = {
Defaults.digitalBookingUser ?? DigitalBookingUser()
}()
internal func setupLabel(label: Label) {
label.font = Fonts.digitalBookingFieldLabel
label.textColor = .grey1
label.textAlignment = .left
}
internal func addView(_ view: UIView) {
stackView.addArrangedSubview(view)
setWidth(view: view)
}
private func setWidth(view: UIView) {
view.snp.makeConstraints { make in
make.width.equalToSuperview().priority(999)
}
}
}
// MARK: - Actions
extension DigitalBookingViewController {
@objc func nextScreen() {
delegate?.didSelectNext()
}
func saveUser() {
Defaults.digitalBookingUser = user
}
}
// MARK: - Observation
extension DigitalBookingViewController {
@objc func keyboardDidShow() {
guard let textView = editingTextField else { return }
let rect = textView.convert(textView.bounds, to: scrollView)
scrollView.scrollRectToVisible(rect, animated: true)
textView.becomeFirstResponder()
}
}
// MARK: - UITextFieldDelegate
extension DigitalBookingViewController: UITextFieldDelegate {
@objc internal func textFieldDidChange(_ sender: UITextField) {
guard let text = sender.text else { return }
text.isEmpty ? nextButton.disable() : nextButton.enable()
}
internal func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard
let text = textField.text,
!text.isEmpty
else { return false }
view.endEditing(true)
nextScreen()
return true
}
internal func textFieldDidBeginEditing(_ textField: UITextField) {
editingTextField = textField
textField.underlined(color: .pink)
}
internal func textFieldDidEndEditing(_ textField: UITextField) {
textField.underlined(color: .grey4)
}
}
// MARK: - Lifecycle
extension DigitalBookingViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
observer = registerKeyboardNotification(scrollView: scrollView)
keyboardObserver.observe { [weak self] (event) -> Void in
guard let digitalBookingViewController = self else { return }
switch event.type {
case .didShow:
digitalBookingViewController.keyboardDidShow()
default:
break
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let observer = self.observer {
NotificationCenter.default.removeObserver(observer)
}
editingTextField?.resignFirstResponder()
editingTextField = nil
view.endEditing(true)
}
}
// MARK: - Interface
extension DigitalBookingViewController {
// MARK: Design
override func setupDesign() {
super.setupDesign()
nextButton.setupDesign(with: Strings.digitalBooking_next.localized.uppercased(), style: .pink)
nextButton.enable()
}
// MARK: Bindings
override func setupBindings() {
super.setupBindings()
nextButton.addTarget(self, action: #selector(nextScreen), for: .touchUpInside)
hideKeyboardWhenTappedAround()
}
// MARK: Layout
override func setupLayout() {
super.setupLayout()
view.addSubview(scrollView)
view.addSubview(nextButton)
scrollView.addSubview(stackView)
stackView.addArrangedSubview(spacingView)
stackView.axis = .vertical
stackView.alignment = .center
scrollView.snp.remakeConstraints { make in
make.top.equalTo(topLayoutGuide.snp.bottom)
make.left.right.equalToSuperview()
}
stackView.snp.makeConstraints { make in
make.edges.width.equalToSuperview().inset(Margins.large)
}
spacingView.snp.makeConstraints { make in
make.height.equalTo(Margins.Spacing.small)
}
nextButton.snp.makeConstraints { make in
make.height.equalTo(Sizes.buttonHeight)
make.left.right.equalToSuperview().inset(Margins.large)
make.bottom.equalTo(bottomLayoutGuide.snp.top).offset(-Margins.large)
make.top.equalTo(scrollView.snp.bottom).offset(Margins.large)
}
}
}
子类如下:
import UIKit
class AboutMeNameViewController: DigitalBookingViewController {
// MARK: Interface Properties
private let headerLabel = SectionHeaderView()
private let nameLabel = Label()
private let nameTextField = QuestionaryTextField()
// MARK: Overrides
override func nextScreen() {
user.name = nameTextField.text
saveUser()
super.nextScreen()
}
}
// MARK: - Lifecycle
extension AboutMeNameViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
nameTextField.becomeFirstResponder()
editingTextField = nameTextField
}
}
我的PageViewController
:
import UIKit
class DigitalBookingPageViewController: PageViewController {
// MARK: Private Properties
private var allViewControllers = [[DigitalBookingBaseViewController]]()
// MARK: Internal Properties
internal var numberOfSteps: Int {
return allViewControllers.flatMap { $0 }.count - 1
}
private var currentPage = Defaults.digitalBookingPage ?? 0
private var currentSection = Defaults.digitalBookingSection ?? 0
weak var mainDBDelegate: MainDigitalBookingDelegate?
// MARK: Initializers
init() {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
setupViewControllers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension DigitalBookingPageViewController {
private func saveLocation() {
Defaults.digitalBookingPage = currentPage
Defaults.digitalBookingSection = currentSection
}
func scrollToNext() -> Bool {
if currentPage + 1 == allViewControllers[currentSection].count
&& currentSection + 1 == allViewControllers.count {
return false
}
currentPage = currentPage + 1 < allViewControllers[currentSection].count ? currentPage + 1 : 0
currentSection = currentPage == 0 ? currentSection + 1 : currentSection
let viewController = allViewControllers[currentSection][currentPage]
setViewControllers([viewController], direction: .forward, animated: true, completion: { _ in
self.mainDBDelegate?.didFinishAnimation()
})
saveLocation()
return true
}
func scrollToPrevious() -> Bool{
if currentPage == 0 && currentSection == 0 { return false }
if currentPage - 1 < 0 {
currentSection = currentSection - 1 < 0 ? 0 : currentSection - 1
currentPage = allViewControllers[currentSection].count - 1
} else {
currentPage -= 1
}
let viewController = allViewControllers[currentSection][currentPage]
setViewControllers([viewController], direction: .reverse, animated: true, completion: { _ in
self.mainDBDelegate?.didFinishAnimation()
})
saveLocation()
return true
}
}
// MARK: - Private Functions
private extension DigitalBookingPageViewController {
private func setupViewControllers() {
var generalInfoSection = [DigitalBookingBaseViewController]()
generalInfoSection.append(AboutMeFirstViewController())
generalInfoSection.append(AboutMeNameViewController())
generalInfoSection.append(AboutMeSurnameViewController())
generalInfoSection.append(AboutMeBirthdayViewController())
generalInfoSection.append(AboutMeDrivingLicenseViewController())
var incomeSection = [DigitalBookingBaseViewController]()
incomeSection.append(AboutMeIncomeFirstViewController())
incomeSection.append(AboutMeIncomeSelectorViewController())
incomeSection.append(AboutMeExpensesViewController())
incomeSection.append(AboutMeEmployeerViewController())
incomeSection.append(AboutMeIncomeFinalViewController())
var contactInfoSection = [DigitalBookingBaseViewController]()
contactInfoSection.append(ContactInformationViewController())
contactInfoSection.append(ContactInformationBankViewController())
contactInfoSection.append(ContactInformationLoginViewController())
contactInfoSection.append(ContactInformationVerifyEmailViewController())
contactInfoSection.append(ContactInformationAutoReserved())
allViewControllers.append(contentsOf: [generalInfoSection, incomeSection, contactInfoSection])
allViewControllers.forEach { $0.forEach { $0.delegate = self } }
let firstViewController = allViewControllers[currentSection][currentPage]
setViewControllers([firstViewController], direction: .forward, animated: true)
}
}
// MARK: - DigitalBookingPageControllerDelegate
extension DigitalBookingPageViewController: DigitalBookingPageControllerDelegate {
func didSelectNext() {
mainDBDelegate?.didMoveToNextPage()
}
func didFinishDigitalBooking() {
mainDBDelegate?.didFinishDigitalBooking()
}
}
最后是MainViewController
推动的NavigationController
:
import MessageUI
protocol DigitalBookingMainViewControllerDelegate: class {
func didFinishDigitalBooking(_ controller: DigitalBookingMainViewController)
}
class DigitalBookingMainViewController: ViewController {
// MARK: Interface Properties
private var progressView: ProgressView?
private let questionaryPageViewController = DigitalBookingPageViewController()
weak var delegate: DigitalBookingMainViewControllerDelegate?
// MARK: Initializers
init() {
super.init(nibName: nil, bundle: nil)
progressView = ProgressView(numberOfSteps: questionaryPageViewController.numberOfSteps)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Actions
extension DigitalBookingMainViewController {
@objc func nextScreen() {
if questionaryPageViewController.scrollToNext() { progressView?.increaseProgress() }
}
func previousScreen() {
if questionaryPageViewController.scrollToPrevious() { progressView?.decreaseProgress() }
}
@objc func dismissViewController() {
dismiss(animated: true)
}
}
// MARK: - DigitalBookingDelegate
extension DigitalBookingMainViewController: MainDigitalBookingDelegate {
@objc func didMoveToPreviousPage() {
UIApplication.shared.windows.first?.endEditing(true)
navigationItem.leftBarButtonItem?.isEnabled = false
progressView?.decreaseProgress()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [unowned self] in
self.previousScreen()
}
}
@objc func didMoveToNextPage() {
UIApplication.shared.windows.first?.endEditing(true)
progressView?.increaseProgress()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [unowned self] in
self.nextScreen()
}
}
func hideProgressBar() {
progressView?.isHidden = true
}
func showProgressBar() {
progressView?.isHidden = false
}
func didFinishDigitalBooking() {
delegate?.didFinishDigitalBooking(self)
self.navigationController?.dismiss(animated: true)
}
func didFinishAnimation() {
navigationItem.leftBarButtonItem?.isEnabled = true
}
}
// MARK: - Interface
extension DigitalBookingMainViewController {
// MARK: Design
override func setupDesign() {
super.setupDesign()
navigationController?.navigationBar.tintColor = .pink
}
// MARK: Strings
override func setupStrings() {
super.setupStrings()
title = Strings.digitalBooking_booking.localized
}
// MARK: Bindings
override func setupBindings() {
super.setupBindings()
questionaryPageViewController.mainDBDelegate = self
addChildViewController(questionaryPageViewController)
questionaryPageViewController.didMove(toParentViewController: self)
// TODO: Fix navigation bug
navigationItem.leftBarButtonItem = UIBarButtonItem(title: Strings.digitalBooking_back.localized, style: .plain, target: self, action: #selector(didMoveToPreviousPage))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: Strings.digitalBooking_cancel.localized, style: .plain, target: self, action: #selector(dismissViewController))
}
// MARK: Layout
override func setupLayout() {
super.setupLayout()
guard let progressView = progressView else { return }
view.addSubview(progressView)
view.addSubview(questionaryPageViewController.view)
progressView.snp.makeConstraints { make in
make.top.equalTo(topLayoutGuide.snp.bottom)
make.left.right.equalToSuperview()
make.height.equalTo(Sizes.progressBarHeight)
}
questionaryPageViewController.view.snp.makeConstraints { make in
make.top.equalTo(progressView.snp.bottom)
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
}
}
}