添加网络层并接入 v9 登录流程及单元测试
This commit is contained in:
318
suixinkan/UI/Login/AccountSelectionViewController.swift
Normal file
318
suixinkan/UI/Login/AccountSelectionViewController.swift
Normal file
@ -0,0 +1,318 @@
|
||||
//
|
||||
// AccountSelectionViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
private typealias AccountSelectionSection = Int
|
||||
private typealias AccountSelectionItem = String
|
||||
|
||||
/// 多账号登录时的账号选择页,展示景区/门店账号列表并提交选择。
|
||||
final class AccountSelectionViewController: UIViewController, UITableViewDelegate {
|
||||
|
||||
private let payload: AccountSelectionPayload
|
||||
private var isLoading: Bool
|
||||
private let onCancel: () -> Void
|
||||
private let onConfirm: (AccountSwitchAccount) -> Void
|
||||
|
||||
private var selectedAccountId: String?
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private var tableDataSource: UITableViewDiffableDataSource<AccountSelectionSection, AccountSelectionItem>!
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
private let bottomBar = UIView()
|
||||
|
||||
init(
|
||||
payload: AccountSelectionPayload,
|
||||
isLoading: Bool,
|
||||
onCancel: @escaping () -> Void,
|
||||
onConfirm: @escaping (AccountSwitchAccount) -> Void
|
||||
) {
|
||||
self.payload = payload
|
||||
self.isLoading = isLoading
|
||||
self.onCancel = onCancel
|
||||
self.onConfirm = onConfirm
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "选择账号"
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FB)
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
title: "取消",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(cancelTapped)
|
||||
)
|
||||
isModalInPresentation = isLoading
|
||||
|
||||
selectedAccountId = payload.accounts.first?.id
|
||||
configureTableView()
|
||||
configureTableDataSource()
|
||||
configureBottomBar()
|
||||
}
|
||||
|
||||
/// 更新 Loading 状态。
|
||||
func updateLoading(_ loading: Bool) {
|
||||
isLoading = loading
|
||||
isModalInPresentation = loading
|
||||
confirmButton.isEnabled = canConfirm
|
||||
confirmButton.backgroundColor = canConfirm ? AppColor.primary : AppColor.buttonDisabled
|
||||
}
|
||||
|
||||
private var selectedAccount: AccountSwitchAccount? {
|
||||
payload.accounts.first { $0.id == selectedAccountId }
|
||||
}
|
||||
|
||||
private var canConfirm: Bool {
|
||||
selectedAccount != nil && !isLoading
|
||||
}
|
||||
|
||||
private func configureTableDataSource() {
|
||||
tableDataSource = UITableViewDiffableDataSource<AccountSelectionSection, AccountSelectionItem>(
|
||||
tableView: tableView
|
||||
) { [weak self] tableView, indexPath, item in
|
||||
guard let self,
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: AccountSelectionCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? AccountSelectionCell,
|
||||
let account = self.payload.accounts.first(where: { $0.id == item }) else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.configure(account: account, selected: account.id == self.selectedAccountId)
|
||||
return cell
|
||||
}
|
||||
applyTableSnapshot(animated: false)
|
||||
}
|
||||
|
||||
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<AccountSelectionSection, AccountSelectionItem> {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<AccountSelectionSection, AccountSelectionItem>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(payload.accounts.map(\.id), toSection: 0)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
|
||||
var snapshot = buildTableSnapshot()
|
||||
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
|
||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||
}
|
||||
tableDataSource.apply(snapshot, animatingDifferences: animated)
|
||||
}
|
||||
|
||||
private func configureTableView() {
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.register(AccountSelectionCell.self, forCellReuseIdentifier: AccountSelectionCell.reuseIdentifier)
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(90)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureBottomBar() {
|
||||
bottomBar.backgroundColor = .white
|
||||
|
||||
let divider = UIView()
|
||||
divider.backgroundColor = UIColor.separator
|
||||
|
||||
confirmButton.setTitle("进入系统", for: .normal)
|
||||
confirmButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
confirmButton.setTitleColor(.white, for: .normal)
|
||||
confirmButton.layer.cornerRadius = 8
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
updateLoading(isLoading)
|
||||
|
||||
view.addSubview(bottomBar)
|
||||
bottomBar.addSubview(divider)
|
||||
bottomBar.addSubview(confirmButton)
|
||||
|
||||
bottomBar.snp.makeConstraints { make in
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
|
||||
divider.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(0.5)
|
||||
}
|
||||
|
||||
confirmButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(divider.snp.bottom).offset(12)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(14)
|
||||
make.height.equalTo(50)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
onCancel()
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc private func confirmTapped() {
|
||||
guard let selectedAccount else { return }
|
||||
onConfirm(selectedAccount)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
selectedAccountId = item
|
||||
applyTableSnapshot(animated: false, reconfigure: true)
|
||||
confirmButton.isEnabled = canConfirm
|
||||
confirmButton.backgroundColor = canConfirm ? AppColor.primary : AppColor.buttonDisabled
|
||||
}
|
||||
}
|
||||
|
||||
/// 账号选择列表 Cell。
|
||||
private final class AccountSelectionCell: UITableViewCell {
|
||||
static let reuseIdentifier = "AccountSelectionCell"
|
||||
|
||||
private let avatarView = UIView()
|
||||
private let avatarLabel = UILabel()
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let phoneLabel = UILabel()
|
||||
private let typeTag = UILabel()
|
||||
private let currentTag = UILabel()
|
||||
private let checkmark = UIImageView()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .white
|
||||
contentView.layer.cornerRadius = 8
|
||||
contentView.layer.masksToBounds = true
|
||||
|
||||
avatarView.layer.cornerRadius = 25
|
||||
|
||||
avatarLabel.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLabel.textColor = AppColor.text333
|
||||
|
||||
subtitleLabel.font = .systemFont(ofSize: 13)
|
||||
subtitleLabel.textColor = AppColor.text666
|
||||
|
||||
phoneLabel.font = .systemFont(ofSize: 12)
|
||||
phoneLabel.textColor = UIColor(hex: 0x9AA1AA)
|
||||
|
||||
typeTag.font = .systemFont(ofSize: 11, weight: .semibold)
|
||||
typeTag.textAlignment = .center
|
||||
typeTag.layer.cornerRadius = 4
|
||||
typeTag.clipsToBounds = true
|
||||
|
||||
currentTag.font = .systemFont(ofSize: 11, weight: .semibold)
|
||||
currentTag.textAlignment = .center
|
||||
currentTag.text = "当前"
|
||||
currentTag.textColor = AppColor.primary
|
||||
currentTag.backgroundColor = AppColor.secondaryButtonBackground
|
||||
currentTag.layer.cornerRadius = 4
|
||||
currentTag.clipsToBounds = true
|
||||
currentTag.isHidden = true
|
||||
|
||||
checkmark.contentMode = .scaleAspectFit
|
||||
|
||||
contentView.addSubview(avatarView)
|
||||
avatarView.addSubview(avatarLabel)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(subtitleLabel)
|
||||
contentView.addSubview(phoneLabel)
|
||||
contentView.addSubview(typeTag)
|
||||
contentView.addSubview(currentTag)
|
||||
contentView.addSubview(checkmark)
|
||||
|
||||
avatarView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().inset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(50)
|
||||
}
|
||||
|
||||
avatarLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().inset(16)
|
||||
make.leading.equalTo(avatarView.snp.trailing).offset(12)
|
||||
make.trailing.lessThanOrEqualTo(checkmark.snp.leading).offset(-8)
|
||||
}
|
||||
|
||||
currentTag.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(6)
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.leading.equalTo(titleLabel)
|
||||
make.trailing.equalTo(checkmark.snp.leading).offset(-8)
|
||||
}
|
||||
|
||||
phoneLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(subtitleLabel.snp.bottom).offset(4)
|
||||
make.leading.equalTo(titleLabel)
|
||||
make.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
|
||||
typeTag.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(checkmark.snp.leading).offset(-8)
|
||||
make.top.equalTo(titleLabel)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
checkmark.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(22)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
nil
|
||||
}
|
||||
|
||||
func configure(account: AccountSwitchAccount, selected: Bool) {
|
||||
titleLabel.text = account.title.isEmpty ? account.accountTypeLabel : account.title
|
||||
subtitleLabel.text = account.subtitle
|
||||
subtitleLabel.isHidden = account.subtitle.isEmpty
|
||||
phoneLabel.text = account.phone
|
||||
phoneLabel.isHidden = account.phone.isEmpty
|
||||
currentTag.isHidden = !account.isCurrent
|
||||
|
||||
if account.isStoreUser {
|
||||
avatarView.backgroundColor = UIColor(hex: 0xE8F8F1)
|
||||
avatarLabel.text = "店"
|
||||
avatarLabel.textColor = UIColor(hex: 0x0F9F6E)
|
||||
typeTag.text = " 门店 "
|
||||
typeTag.textColor = UIColor(hex: 0x0F9F6E)
|
||||
typeTag.backgroundColor = UIColor(hex: 0xE8F8F1)
|
||||
} else {
|
||||
avatarView.backgroundColor = UIColor(hex: 0xF3ECFF)
|
||||
avatarLabel.text = "景"
|
||||
avatarLabel.textColor = UIColor(hex: 0x7C3AED)
|
||||
typeTag.text = " 景区 "
|
||||
typeTag.textColor = UIColor(hex: 0x7C3AED)
|
||||
typeTag.backgroundColor = UIColor(hex: 0xF3ECFF)
|
||||
}
|
||||
|
||||
let symbolName = selected ? "checkmark.circle.fill" : "circle"
|
||||
checkmark.image = UIImage(systemName: symbolName)
|
||||
checkmark.tintColor = selected ? AppColor.primary : UIColor(hex: 0xB6BECA)
|
||||
|
||||
contentView.layer.borderWidth = selected ? 1.4 : 1
|
||||
contentView.layer.borderColor = (selected ? AppColor.primary : UIColor(hex: 0xE6ECF4)).cgColor
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user