IOS-129: Redesign onboarding buttons and add loading indicator on Next-Button (#989)
This commit is contained in:
parent
ee8f5d6f51
commit
0c239a7882
|
@ -21,8 +21,6 @@ class MastodonLoginView: UIView {
|
||||||
private let searchContainerLeftPaddingView: UIView
|
private let searchContainerLeftPaddingView: UIView
|
||||||
|
|
||||||
let tableView: UITableView
|
let tableView: UITableView
|
||||||
let navigationActionView: NavigationActionView
|
|
||||||
var bottomConstraint: NSLayoutConstraint?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
|
||||||
|
@ -64,15 +62,11 @@ class MastodonLoginView: UIView {
|
||||||
tableView.keyboardDismissMode = .onDrag
|
tableView.keyboardDismissMode = .onDrag
|
||||||
tableView.layer.cornerRadius = 10
|
tableView.layer.cornerRadius = 10
|
||||||
|
|
||||||
navigationActionView = NavigationActionView()
|
|
||||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
addSubview(explanationTextLabel)
|
addSubview(explanationTextLabel)
|
||||||
addSubview(searchTextField)
|
addSubview(searchTextField)
|
||||||
addSubview(tableView)
|
addSubview(tableView)
|
||||||
addSubview(navigationActionView)
|
|
||||||
backgroundColor = Asset.Scene.Onboarding.background.color
|
backgroundColor = Asset.Scene.Onboarding.background.color
|
||||||
|
|
||||||
setupConstraints()
|
setupConstraints()
|
||||||
|
@ -84,8 +78,6 @@ class MastodonLoginView: UIView {
|
||||||
|
|
||||||
private func setupConstraints() {
|
private func setupConstraints() {
|
||||||
|
|
||||||
let bottomConstraint = safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor)
|
|
||||||
|
|
||||||
let constraints = [
|
let constraints = [
|
||||||
explanationTextLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
explanationTextLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||||
explanationTextLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
explanationTextLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
@ -109,14 +101,9 @@ class MastodonLoginView: UIView {
|
||||||
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2),
|
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2),
|
||||||
tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||||
trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16),
|
trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16),
|
||||||
tableView.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor),
|
tableView.bottomAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.bottomAnchor),
|
||||||
|
|
||||||
navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
bottomConstraint,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.bottomConstraint = bottomConstraint
|
|
||||||
NSLayoutConstraint.activate(constraints)
|
NSLayoutConstraint.activate(constraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,240 +14,264 @@ import AuthenticationServices
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
|
||||||
protocol MastodonLoginViewControllerDelegate: AnyObject {
|
protocol MastodonLoginViewControllerDelegate: AnyObject {
|
||||||
func backButtonPressed(_ viewController: MastodonLoginViewController)
|
func backButtonPressed(_ viewController: MastodonLoginViewController)
|
||||||
func nextButtonPressed(_ viewController: MastodonLoginViewController)
|
func nextButtonPressed(_ viewController: MastodonLoginViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MastodonLoginViewSection: Hashable {
|
enum MastodonLoginViewSection: Hashable {
|
||||||
case servers
|
case servers
|
||||||
}
|
}
|
||||||
|
|
||||||
class MastodonLoginViewController: UIViewController, NeedsDependency {
|
class MastodonLoginViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
weak var delegate: MastodonLoginViewControllerDelegate?
|
enum RightBarButtonState {
|
||||||
var dataSource: UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>?
|
case normal, disabled, loading
|
||||||
let viewModel: MastodonLoginViewModel
|
|
||||||
let authenticationViewModel: AuthenticationViewModel
|
|
||||||
var mastodonAuthenticationController: MastodonAuthenticationController?
|
|
||||||
|
|
||||||
weak var context: AppContext!
|
|
||||||
weak var coordinator: SceneCoordinator!
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
var contentView: MastodonLoginView {
|
|
||||||
view as! MastodonLoginView
|
|
||||||
}
|
|
||||||
|
|
||||||
init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel, sceneCoordinator: SceneCoordinator) {
|
|
||||||
|
|
||||||
viewModel = MastodonLoginViewModel(appContext: appContext)
|
|
||||||
self.authenticationViewModel = authenticationViewModel
|
|
||||||
self.context = appContext
|
|
||||||
self.coordinator = sceneCoordinator
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
viewModel.delegate = self
|
|
||||||
|
|
||||||
navigationItem.hidesBackButton = true
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
||||||
|
|
||||||
override func loadView() {
|
|
||||||
let loginView = MastodonLoginView()
|
|
||||||
|
|
||||||
loginView.navigationActionView.nextButton.addTarget(self, action: #selector(MastodonLoginViewController.nextButtonPressed(_:)), for: .touchUpInside)
|
|
||||||
loginView.navigationActionView.backButton.addTarget(self, action: #selector(MastodonLoginViewController.backButtonPressed(_:)), for: .touchUpInside)
|
|
||||||
loginView.searchTextField.addTarget(self, action: #selector(MastodonLoginViewController.textfieldDidChange(_:)), for: .editingChanged)
|
|
||||||
loginView.tableView.delegate = self
|
|
||||||
loginView.tableView.register(MastodonLoginServerTableViewCell.self, forCellReuseIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier)
|
|
||||||
loginView.navigationActionView.nextButton.isEnabled = false
|
|
||||||
|
|
||||||
view = loginView
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
||||||
|
|
||||||
let dataSource = UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in
|
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell,
|
|
||||||
let self = self else {
|
|
||||||
fatalError("Wrong cell")
|
|
||||||
}
|
|
||||||
|
|
||||||
let server = self.viewModel.filteredServers[indexPath.row]
|
|
||||||
var configuration = cell.defaultContentConfiguration()
|
|
||||||
configuration.text = server.domain
|
|
||||||
|
|
||||||
cell.contentConfiguration = configuration
|
|
||||||
cell.accessoryType = .disclosureIndicator
|
|
||||||
|
|
||||||
cell.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentView.tableView.dataSource = dataSource
|
weak var delegate: MastodonLoginViewControllerDelegate?
|
||||||
self.dataSource = dataSource
|
var dataSource: UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>?
|
||||||
|
let viewModel: MastodonLoginViewModel
|
||||||
|
let authenticationViewModel: AuthenticationViewModel
|
||||||
|
var mastodonAuthenticationController: MastodonAuthenticationController?
|
||||||
|
|
||||||
contentView.updateCorners()
|
weak var context: AppContext!
|
||||||
|
weak var coordinator: SceneCoordinator!
|
||||||
|
|
||||||
defer { setupNavigationBarBackgroundView() }
|
var disposeBag = Set<AnyCancellable>()
|
||||||
setupOnboardingAppearance()
|
|
||||||
|
|
||||||
title = L10n.Scene.Login.title
|
var contentView: MastodonLoginView {
|
||||||
navigationItem.hidesBackButton = true
|
view as! MastodonLoginView
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel, sceneCoordinator: SceneCoordinator) {
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
viewModel.updateServers()
|
viewModel = MastodonLoginViewModel(appContext: appContext)
|
||||||
}
|
self.authenticationViewModel = authenticationViewModel
|
||||||
|
self.context = appContext
|
||||||
|
self.coordinator = sceneCoordinator
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
super.init(nibName: nil, bundle: nil)
|
||||||
super.viewDidAppear(animated)
|
viewModel.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
contentView.searchTextField.becomeFirstResponder()
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: - Actions
|
override func loadView() {
|
||||||
|
let loginView = MastodonLoginView()
|
||||||
|
|
||||||
@objc func backButtonPressed(_ sender: Any) {
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
contentView.searchTextField.resignFirstResponder()
|
title: L10n.Common.Controls.Actions.next,
|
||||||
delegate?.backButtonPressed(self)
|
style: .plain,
|
||||||
}
|
target: self,
|
||||||
|
action: #selector(nextButtonPressed(_:))
|
||||||
@objc func nextButtonPressed(_ sender: Any) {
|
|
||||||
contentView.searchTextField.resignFirstResponder()
|
|
||||||
delegate?.nextButtonPressed(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func login() {
|
|
||||||
guard let server = viewModel.selectedServer else { return }
|
|
||||||
|
|
||||||
authenticationViewModel
|
|
||||||
.authenticated
|
|
||||||
.asyncMap { domain, user -> Result<Bool, Error> in
|
|
||||||
do {
|
|
||||||
let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
|
||||||
return .success(result)
|
|
||||||
} catch {
|
|
||||||
return .failure(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] result in
|
|
||||||
guard let self = self else { return }
|
|
||||||
switch result {
|
|
||||||
case .failure(let error):
|
|
||||||
assertionFailure(error.localizedDescription)
|
|
||||||
case .success(let isActived):
|
|
||||||
assert(isActived)
|
|
||||||
self.coordinator.setup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
authenticationViewModel.isAuthenticating.send(true)
|
|
||||||
context.apiService.createApplication(domain: server.domain)
|
|
||||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
|
||||||
let application = response.value
|
|
||||||
guard let info = AuthenticationViewModel.AuthenticateInfo(
|
|
||||||
domain: server.domain,
|
|
||||||
application: application,
|
|
||||||
redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL
|
|
||||||
) else {
|
|
||||||
throw APIService.APIError.explicit(.badResponse)
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] completion in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.authenticationViewModel.isAuthenticating.send(false)
|
|
||||||
|
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
|
||||||
let alert = UIAlertController.standardAlert(of: error)
|
|
||||||
self.present(alert, animated: true)
|
|
||||||
case .finished:
|
|
||||||
// do nothing. There's a subscriber above resulting in `coordinator.setup()`
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} receiveValue: { [weak self] info in
|
|
||||||
guard let self else { return }
|
|
||||||
let authenticationController = MastodonAuthenticationController(
|
|
||||||
context: self.context,
|
|
||||||
authenticateURL: info.authorizeURL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.mastodonAuthenticationController = authenticationController
|
navigationItem.leftBarButtonItem?.target = self
|
||||||
authenticationController.authenticationSession?.presentationContextProvider = self
|
navigationItem.leftBarButtonItem?.action = #selector(backButtonPressed(_:))
|
||||||
authenticationController.authenticationSession?.start()
|
|
||||||
|
|
||||||
self.authenticationViewModel.authenticate(
|
loginView.searchTextField.addTarget(self, action: #selector(MastodonLoginViewController.textfieldDidChange(_:)), for: .editingChanged)
|
||||||
info: info,
|
loginView.tableView.delegate = self
|
||||||
pinCodePublisher: authenticationController.pinCodePublisher
|
loginView.tableView.register(MastodonLoginServerTableViewCell.self, forCellReuseIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier)
|
||||||
)
|
setRightBarButtonState(.disabled)
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func textfieldDidChange(_ textField: UITextField) {
|
view = loginView
|
||||||
viewModel.filterServers(withText: textField.text)
|
|
||||||
|
|
||||||
|
|
||||||
if let text = textField.text,
|
|
||||||
let domain = AuthenticationViewModel.parseDomain(from: text) {
|
|
||||||
|
|
||||||
viewModel.selectedServer = .init(domain: domain, instance: .init(domain: domain))
|
|
||||||
contentView.navigationActionView.nextButton.isEnabled = true
|
|
||||||
} else {
|
|
||||||
viewModel.selectedServer = nil
|
|
||||||
contentView.navigationActionView.nextButton.isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Notifications
|
|
||||||
@objc func keyboardWillShowNotification(_ notification: Notification) {
|
|
||||||
|
|
||||||
guard let userInfo = notification.userInfo,
|
|
||||||
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
|
|
||||||
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
// inspired by https://stackoverflow.com/a/30245044
|
|
||||||
let keyboardFrame = keyboardFrameValue.cgRectValue
|
|
||||||
|
|
||||||
let keyboardOrigin = view.convert(keyboardFrame.origin, from: nil)
|
|
||||||
let intersectionY = CGRectGetMaxY(view.frame) - keyboardOrigin.y;
|
|
||||||
|
|
||||||
if intersectionY >= 0 {
|
|
||||||
contentView.bottomConstraint?.constant = intersectionY - view.safeAreaInsets.bottom
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
override func viewDidLoad() {
|
||||||
self.view.layoutIfNeeded()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||||
|
|
||||||
|
let dataSource = UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in
|
||||||
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell,
|
||||||
|
let self = self else {
|
||||||
|
fatalError("Wrong cell")
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = self.viewModel.filteredServers[indexPath.row]
|
||||||
|
var configuration = cell.defaultContentConfiguration()
|
||||||
|
configuration.text = server.domain
|
||||||
|
|
||||||
|
cell.contentConfiguration = configuration
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
|
||||||
|
cell.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.tableView.dataSource = dataSource
|
||||||
|
self.dataSource = dataSource
|
||||||
|
|
||||||
|
contentView.updateCorners()
|
||||||
|
|
||||||
|
defer { setupNavigationBarBackgroundView() }
|
||||||
|
setupOnboardingAppearance()
|
||||||
|
|
||||||
|
title = L10n.Scene.Login.title
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@objc func keyboardWillHideNotification(_ notification: Notification) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
guard let userInfo = notification.userInfo,
|
viewModel.updateServers()
|
||||||
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
}
|
||||||
else { return }
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
contentView.bottomConstraint?.constant = 0
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
contentView.searchTextField.becomeFirstResponder()
|
||||||
self.view.layoutIfNeeded()
|
}
|
||||||
|
|
||||||
|
//MARK: - Actions
|
||||||
|
|
||||||
|
@objc func backButtonPressed(_ sender: Any) {
|
||||||
|
contentView.searchTextField.resignFirstResponder()
|
||||||
|
delegate?.backButtonPressed(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func nextButtonPressed(_ sender: Any) {
|
||||||
|
contentView.searchTextField.resignFirstResponder()
|
||||||
|
delegate?.nextButtonPressed(self)
|
||||||
|
setRightBarButtonState(.loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func login() {
|
||||||
|
guard let server = viewModel.selectedServer else { return }
|
||||||
|
|
||||||
|
authenticationViewModel
|
||||||
|
.authenticated
|
||||||
|
.asyncMap { domain, user -> Result<Bool, Error> in
|
||||||
|
do {
|
||||||
|
let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
||||||
|
return .success(result)
|
||||||
|
} catch {
|
||||||
|
return .failure(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
case .success(let isActived):
|
||||||
|
assert(isActived)
|
||||||
|
self.coordinator.setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
authenticationViewModel.isAuthenticating.send(true)
|
||||||
|
context.apiService.createApplication(domain: server.domain)
|
||||||
|
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||||
|
let application = response.value
|
||||||
|
guard let info = AuthenticationViewModel.AuthenticateInfo(
|
||||||
|
domain: server.domain,
|
||||||
|
application: application,
|
||||||
|
redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL
|
||||||
|
) else {
|
||||||
|
throw APIService.APIError.explicit(.badResponse)
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.authenticationViewModel.isAuthenticating.send(false)
|
||||||
|
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
let alert = UIAlertController.standardAlert(of: error)
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
self.setRightBarButtonState(.normal)
|
||||||
|
case .finished:
|
||||||
|
self.setRightBarButtonState(.normal)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] info in
|
||||||
|
guard let self else { return }
|
||||||
|
let authenticationController = MastodonAuthenticationController(
|
||||||
|
context: self.context,
|
||||||
|
authenticateURL: info.authorizeURL
|
||||||
|
)
|
||||||
|
|
||||||
|
self.mastodonAuthenticationController = authenticationController
|
||||||
|
authenticationController.authenticationSession?.presentationContextProvider = self
|
||||||
|
authenticationController.authenticationSession?.start()
|
||||||
|
|
||||||
|
self.authenticationViewModel.authenticate(
|
||||||
|
info: info,
|
||||||
|
pinCodePublisher: authenticationController.pinCodePublisher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func textfieldDidChange(_ textField: UITextField) {
|
||||||
|
viewModel.filterServers(withText: textField.text)
|
||||||
|
|
||||||
|
|
||||||
|
if let text = textField.text,
|
||||||
|
let domain = AuthenticationViewModel.parseDomain(from: text) {
|
||||||
|
|
||||||
|
viewModel.selectedServer = .init(domain: domain, instance: .init(domain: domain))
|
||||||
|
setRightBarButtonState(.normal)
|
||||||
|
} else {
|
||||||
|
viewModel.selectedServer = nil
|
||||||
|
setRightBarButtonState(.disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notifications
|
||||||
|
@objc func keyboardWillShowNotification(_ notification: Notification) {
|
||||||
|
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
|
||||||
|
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
// inspired by https://stackoverflow.com/a/30245044
|
||||||
|
let keyboardFrame = keyboardFrameValue.cgRectValue
|
||||||
|
|
||||||
|
let keyboardOrigin = view.convert(keyboardFrame.origin, from: nil)
|
||||||
|
let intersectionY = CGRectGetMaxY(view.frame) - keyboardOrigin.y;
|
||||||
|
|
||||||
|
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func keyboardWillHideNotification(_ notification: Notification) {
|
||||||
|
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setRightBarButtonState(_ state: RightBarButtonState) {
|
||||||
|
switch state {
|
||||||
|
case .normal:
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
|
title: L10n.Common.Controls.Actions.next,
|
||||||
|
style: .plain,
|
||||||
|
target: self,
|
||||||
|
action: #selector(nextButtonPressed(_:))
|
||||||
|
)
|
||||||
|
case .disabled:
|
||||||
|
navigationItem.rightBarButtonItem?.isEnabled = false
|
||||||
|
case .loading:
|
||||||
|
let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
let barButtonItem = UIBarButtonItem(customView: activityIndicator)
|
||||||
|
navigationItem.rightBarButtonItem = barButtonItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - OnboardingViewControllerAppearance
|
// MARK: - OnboardingViewControllerAppearance
|
||||||
|
@ -255,38 +279,38 @@ extension MastodonLoginViewController: OnboardingViewControllerAppearance { }
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension MastodonLoginViewController: UITableViewDelegate {
|
extension MastodonLoginViewController: UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
let server = viewModel.filteredServers[indexPath.row]
|
let server = viewModel.filteredServers[indexPath.row]
|
||||||
viewModel.selectedServer = server
|
viewModel.selectedServer = server
|
||||||
|
|
||||||
contentView.searchTextField.text = server.domain
|
contentView.searchTextField.text = server.domain
|
||||||
viewModel.filterServers(withText: " ")
|
viewModel.filterServers(withText: " ")
|
||||||
|
|
||||||
contentView.navigationActionView.nextButton.isEnabled = true
|
setRightBarButtonState(.normal)
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MastodonLoginViewModelDelegate
|
// MARK: - MastodonLoginViewModelDelegate
|
||||||
extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
|
extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
|
||||||
func serversUpdated(_ viewModel: MastodonLoginViewModel) {
|
func serversUpdated(_ viewModel: MastodonLoginViewModel) {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<MastodonLoginViewSection, Mastodon.Entity.Server>()
|
var snapshot = NSDiffableDataSourceSnapshot<MastodonLoginViewSection, Mastodon.Entity.Server>()
|
||||||
|
|
||||||
snapshot.appendSections([MastodonLoginViewSection.servers])
|
snapshot.appendSections([MastodonLoginViewSection.servers])
|
||||||
snapshot.appendItems(viewModel.filteredServers)
|
snapshot.appendItems(viewModel.filteredServers)
|
||||||
|
|
||||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
OperationQueue.main.addOperation {
|
OperationQueue.main.addOperation {
|
||||||
let numberOfResults = viewModel.filteredServers.count
|
let numberOfResults = viewModel.filteredServers.count
|
||||||
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ASWebAuthenticationPresentationContextProviding
|
// MARK: - ASWebAuthenticationPresentationContextProviding
|
||||||
extension MastodonLoginViewController: ASWebAuthenticationPresentationContextProviding {
|
extension MastodonLoginViewController: ASWebAuthenticationPresentationContextProviding {
|
||||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||||
return view.window!
|
return view.window!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue