Add base infrastructure and SPM dependencies for iOS development.

Integrate SnapKit and Kingfisher, add BaseViewController and NotificationName, and streamline AGENTS.md with Android-aligned conventions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 14:59:55 +08:00
co-authored by Cursor
parent 6cc2336e7b
commit e290656322
6 changed files with 341 additions and 152 deletions
+162
View File
@@ -0,0 +1,162 @@
//
// BaseViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// 业务 ViewController 基类,提供统一的 UI 搭建流程与通用 UI 能力。
class BaseViewController: UIViewController {
private var loadingOverlay: UIView?
private var loadingIndicator: UIActivityIndicatorView?
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupNavigationBar()
setupUI()
setupConstraints()
bindActions()
registerBaseNotifications()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Template Methods
/// 配置导航栏,子类按需重写。
func setupNavigationBar() {}
/// 创建并添加子视图。
func setupUI() {}
/// 使用 SnapKit 设置约束。
func setupConstraints() {}
/// 绑定 target-action、delegate 等交互。
func bindActions() {}
/// Token 失效时回调,子类可重写处理跳转登录等逻辑。
func onSessionDidExpire() {}
// MARK: - Loading
func showLoading() {
if loadingOverlay == nil {
let overlay = UIView()
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.1)
let indicator = UIActivityIndicatorView(style: .large)
indicator.hidesWhenStopped = true
view.addSubview(overlay)
overlay.addSubview(indicator)
overlay.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
indicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
loadingOverlay = overlay
loadingIndicator = indicator
}
loadingOverlay?.isHidden = false
loadingIndicator?.startAnimating()
view.bringSubviewToFront(loadingOverlay!)
}
func hideLoading() {
loadingIndicator?.stopAnimating()
loadingOverlay?.isHidden = true
}
// MARK: - Toast / Alert
func showToast(_ message: String, duration: TimeInterval = 2.0) {
guard !message.isEmpty else { return }
let container = UIView()
container.backgroundColor = UIColor.black.withAlphaComponent(0.8)
container.layer.cornerRadius = 8
container.clipsToBounds = true
container.alpha = 0
let label = UILabel()
label.text = message
label.textColor = .white
label.font = .systemFont(ofSize: 14)
label.textAlignment = .center
label.numberOfLines = 0
container.addSubview(label)
view.addSubview(container)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16))
}
container.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-32)
make.leading.greaterThanOrEqualToSuperview().offset(32)
make.trailing.lessThanOrEqualToSuperview().offset(-32)
}
UIView.animate(withDuration: 0.25) {
container.alpha = 1
} completion: { _ in
UIView.animate(withDuration: 0.25, delay: duration) {
container.alpha = 0
} completion: { _ in
container.removeFromSuperview()
}
}
}
func showError(_ message: String) {
showToast(message)
}
func showAlert(
title: String?,
message: String?,
confirmTitle: String = "确定",
confirmHandler: (() -> Void)? = nil
) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: confirmTitle, style: .default) { _ in
confirmHandler?()
})
present(alert, animated: true)
}
// MARK: - Navigation
func popViewController() {
navigationController?.popViewController(animated: true)
}
// MARK: - Notifications
private func registerBaseNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleSessionDidExpireNotification),
name: NotificationName.sessionDidExpire,
object: nil
)
}
@objc private func handleSessionDidExpireNotification() {
hideLoading()
onSessionDidExpire()
}
}
+74
View File
@@ -0,0 +1,74 @@
//
// NotificationName.swift
// suixinkan
//
import Foundation
/// 工程中所有 `NotificationCenter` 通知名称的统一入口。
/// 新增通知时只能在此文件声明,禁止在业务代码中硬编码字符串。
enum NotificationName {
private static let prefix = "com.yuanzhixiang.suixinkan."
private static func name(_ suffix: String) -> Notification.Name {
Notification.Name(prefix + suffix)
}
// MARK: - Session
/// 用户登录成功
static let userDidLogin = name("userDidLogin")
/// 用户主动退出登录
static let userDidLogout = name("userDidLogout")
/// Token 失效或鉴权失败,需重新登录
static let sessionDidExpire = name("sessionDidExpire")
// MARK: - Scenic
/// 当前景区切换
static let scenicDidChange = name("scenicDidChange")
// MARK: - User Profile
/// 用户资料更新(头像、昵称、角色等)
static let userProfileDidUpdate = name("userProfileDidUpdate")
// MARK: - Message
/// 未读消息数量变化
static let unreadMessageCountDidChange = name("unreadMessageCountDidChange")
// MARK: - Order
/// 订单状态或列表发生变化
static let orderDidUpdate = name("orderDidUpdate")
// MARK: - Task
/// 任务状态或列表发生变化
static let taskDidUpdate = name("taskDidUpdate")
// MARK: - Scenic Queue
/// 排队叫号数据变化
static let scenicQueueDidUpdate = name("scenicQueueDidUpdate")
// MARK: - Voice Call
/// 语音通话状态变化
static let voiceCallStateDidChange = name("voiceCallStateDidChange")
}
/// `Notification.userInfo` 字典键的统一入口。
enum NotificationUserInfoKey {
static let scenicId = "scenicId"
static let scenicName = "scenicName"
static let orderId = "orderId"
static let taskId = "taskId"
static let unreadCount = "unreadCount"
static let voiceCallState = "voiceCallState"
}
+1 -3
View File
@@ -7,13 +7,11 @@
import UIKit
class ViewController: UIViewController {
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}