Files
suixinkan_uikit/suixinkan/UI/Login/LoginViewController.swift

376 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LoginViewController.swift
// suixinkan
//
import SafariServices
import SnapKit
import UIKit
/// UI Android `LoginScreen`
final class LoginViewController: BaseViewController {
private let viewModel = LoginViewModel()
private let authAPI = NetworkServices.shared.authAPI
private let backgroundImageView = UIImageView()
private let welcomeLabel = UILabel()
private let formCardShadowView = UIView()
private let formCardView = UIView()
private let topFormStack = UIStackView()
private let accountField = LoginTextField(placeholder: "请输入账号")
private let passwordField = PasswordInputField()
private let agreementRow = AgreementRowView()
private let loginButton = AppButton(title: "登录", style: .primary)
private let loginByCodeButton = AppButton(title: "验证码登录", style: .secondary)
private let registerContainer = UIStackView()
private let registerPrefixLabel = UILabel()
private let registerButton = UIButton(type: .system)
private weak var accountSelectionController: AccountSelectionViewController?
override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
override func setupNavigationBar() {
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func setupUI() {
view.backgroundColor = .black
backgroundImageView.image = UIImage(named: "img_login_bg")
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.clipsToBounds = true
welcomeLabel.text = "欢迎使用\n随心瞰商家版"
welcomeLabel.numberOfLines = 2
welcomeLabel.font = .systemFont(ofSize: 28, weight: .semibold)
welcomeLabel.textColor = .white
formCardShadowView.backgroundColor = .clear
formCardShadowView.layer.shadowColor = UIColor.black.cgColor
formCardShadowView.layer.shadowOpacity = 0.14
formCardShadowView.layer.shadowOffset = CGSize(width: 0, height: 8)
formCardShadowView.layer.shadowRadius = 20
formCardView.backgroundColor = .white
formCardView.layer.cornerRadius = 16
formCardView.clipsToBounds = true
topFormStack.axis = .vertical
topFormStack.spacing = 0
topFormStack.alignment = .fill
topFormStack.distribution = .fill
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
loginByCodeButton.addTarget(self, action: #selector(loginByCodeButtonTapped), for: .touchUpInside)
registerContainer.axis = .horizontal
registerContainer.alignment = .center
registerContainer.spacing = 0
registerPrefixLabel.text = "没有账号?"
registerPrefixLabel.font = .systemFont(ofSize: 14, weight: .medium)
registerPrefixLabel.textColor = AppColor.text333
registerButton.setTitle("去注册", for: .normal)
registerButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
registerButton.setTitleColor(AppColor.primary, for: .normal)
registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside)
registerContainer.addArrangedSubview(registerPrefixLabel)
registerContainer.addArrangedSubview(registerButton)
view.addSubview(backgroundImageView)
view.addSubview(welcomeLabel)
view.addSubview(formCardShadowView)
formCardShadowView.addSubview(formCardView)
formCardView.addSubview(topFormStack)
formCardView.addSubview(registerContainer)
topFormStack.addArrangedSubview(accountField)
topFormStack.setCustomSpacing(8, after: accountField)
topFormStack.addArrangedSubview(passwordField)
topFormStack.setCustomSpacing(24, after: passwordField)
topFormStack.addArrangedSubview(agreementRow)
topFormStack.setCustomSpacing(16, after: agreementRow)
topFormStack.addArrangedSubview(loginButton)
topFormStack.setCustomSpacing(8, after: loginButton)
topFormStack.addArrangedSubview(loginByCodeButton)
viewModel.applyStoredPreferences()
if let username = viewModel.normalizedUsername.nonEmpty ?? viewModel.account.nonEmpty {
accountField.text = username
}
agreementRow.isChecked = viewModel.isPrivacyChecked
bindViewModel()
updateUI()
}
override func setupConstraints() {
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
welcomeLabel.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(48)
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(24)
}
formCardShadowView.snp.makeConstraints { make in
make.top.equalTo(welcomeLabel.snp.bottom).offset(48)
make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(24)
make.bottom.lessThanOrEqualTo(view.safeAreaLayoutGuide).offset(-24)
}
formCardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
registerContainer.snp.makeConstraints { make in
make.centerX.equalToSuperview()
}
applyCardLayoutConstraints(showRegisterSection: false)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
formCardShadowView.layer.shadowPath = UIBezierPath(
roundedRect: formCardShadowView.bounds,
cornerRadius: 16
).cgPath
}
override func bindActions() {
accountField.onTextChange = { [weak self] text in
self?.viewModel.updateAccount(text)
}
accountField.onReturnKey = { [weak self] in
self?.passwordField.becomeFirstResponder()
}
accountField.onEditingDidEnd = { [weak self] in
self?.viewModel.normalizeUsernameCountryCodeIfNeeded()
if let username = self?.viewModel.normalizedUsername {
self?.accountField.text = username
}
}
passwordField.onTextChange = { [weak self] text in
self?.viewModel.updatePassword(text)
}
passwordField.onReturnKey = { [weak self] in
self?.view.endEditing(true)
self?.viewModel.login()
}
agreementRow.onCheckedChange = { [weak self] in
self?.viewModel.togglePrivacyChecked()
}
agreementRow.onUserAgreementTap = { [weak self] in
self?.viewModel.onUserAgreement?()
}
agreementRow.onPrivacyPolicyTap = { [weak self] in
self?.viewModel.onPrivacyPolicy?()
}
setupDismissKeyboardGesture()
}
private func setupDismissKeyboardGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
tap.delegate = self
view.addGestureRecognizer(tap)
}
@objc private func dismissKeyboard() {
view.endEditing(true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task { await viewModel.loadAppConfig(authAPI: authAPI) }
}
private func bindViewModel() {
viewModel.onStateChange = { [weak self] in
self?.updateUI()
}
viewModel.onShowMessage = { [weak self] message in
self?.showToast(message)
}
viewModel.onLogin = { [weak self] in
self?.performLogin()
}
viewModel.onLoginByCode = { [weak self] in
self?.showToast("验证码登录待接入")
}
viewModel.onRegister = { [weak self] in
self?.showToast("注册待接入")
}
viewModel.onUserAgreement = { [weak self] in
self?.openAgreementURL(AppAgreementURLs.userAgreement)
}
viewModel.onPrivacyPolicy = { [weak self] in
self?.openAgreementURL(AppAgreementURLs.privacyPolicy)
}
}
private func updateUI() {
agreementRow.isChecked = viewModel.isPrivacyChecked
loginButton.isEnabled = viewModel.isLoginEnabled && !viewModel.isLoading
loginButton.alpha = viewModel.isLoading ? 0.7 : 1
let showRegisterSection = viewModel.enableRegister
loginByCodeButton.isHidden = !showRegisterSection
registerContainer.isHidden = !showRegisterSection
applyCardLayoutConstraints(showRegisterSection: showRegisterSection)
if let payload = viewModel.pendingAccountSelection {
if let controller = accountSelectionController {
controller.updateLoading(viewModel.isSelectingAccount)
} else if presentedViewController == nil {
presentAccountSelection(payload)
}
} else if accountSelectionController != nil {
dismiss(animated: true)
accountSelectionController = nil
}
}
/// safeArea 24pt
private func applyCardLayoutConstraints(showRegisterSection: Bool) {
if showRegisterSection {
topFormStack.snp.remakeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
}
registerContainer.snp.remakeConstraints { make in
make.top.equalTo(topFormStack.snp.bottom).offset(16)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(16)
}
} else {
registerContainer.snp.remakeConstraints { make in
make.centerX.equalToSuperview()
}
topFormStack.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
}
}
private func performLogin() {
viewModel.normalizeUsernameCountryCodeIfNeeded()
accountField.text = viewModel.normalizedUsername
Task {
showLoading()
defer { hideLoading() }
do {
let resolution = try await viewModel.login(authAPI: authAPI)
switch resolution {
case let .completed(response, account):
completeLogin(with: response, account: account)
case .needsAccountSelection:
break
}
} catch is CancellationError {
return
} catch {
showToast(error.localizedDescription)
}
}
}
private func presentAccountSelection(_ payload: AccountSelectionPayload) {
let controller = AccountSelectionViewController(
payload: payload,
isLoading: viewModel.isSelectingAccount,
onCancel: { [weak self] in
self?.viewModel.clearPendingAccountSelection()
self?.accountSelectionController = nil
},
onConfirm: { [weak self] account in
self?.selectAccount(account)
}
)
accountSelectionController = controller
let navigation = UINavigationController(rootViewController: controller)
navigation.modalPresentationStyle = .formSheet
present(navigation, animated: true)
}
private func selectAccount(_ account: AccountSwitchAccount) {
Task {
accountSelectionController?.updateLoading(true)
defer { accountSelectionController?.updateLoading(false) }
do {
let response = try await viewModel.selectAccount(account, authAPI: authAPI)
completeLogin(with: response, account: account)
accountSelectionController = nil
dismiss(animated: true)
} catch is CancellationError {
return
} catch {
showToast(error.localizedDescription)
}
}
}
private func completeLogin(with response: V9AuthResponse, account: AccountSwitchAccount) {
AuthSessionHelper.completeLogin(
with: response,
username: viewModel.normalizedUsername,
privacyAgreementAccepted: viewModel.isPrivacyChecked,
selectedAccount: account
)
}
@objc private func loginButtonTapped() {
view.endEditing(true)
viewModel.login()
}
@objc private func loginByCodeButtonTapped() {
view.endEditing(true)
viewModel.onLoginByCode?()
}
@objc private func registerButtonTapped() {
view.endEditing(true)
viewModel.onRegister?()
}
private func openAgreementURL(_ url: URL) {
let controller = SFSafariViewController(url: url)
present(controller, animated: true)
}
}
extension LoginViewController: UIGestureRecognizerDelegate {
///
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
var candidate: UIView? = touch.view
while let view = candidate {
if view is UITextField || view is UIButton {
return false
}
candidate = view.superview
}
return true
}
}
private extension String {
var nonEmpty: String? {
let text = trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
}