Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,62 @@
//
// FeaturePlaceholderViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// UIKit
final class FeaturePlaceholderViewController: UIViewController {
private let pageTitle: String
private let uri: String
init(title: String, uri: String) {
self.pageTitle = title
self.uri = uri
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0xF5F7FA)
title = pageTitle
let icon = UIImageView(image: UIImage(systemName: "square.grid.2x2"))
icon.tintColor = AppDesignUIKit.primary
icon.contentMode = .scaleAspectFit
let titleLabel = UILabel()
titleLabel.text = pageTitle
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title2, weight: .semibold)
titleLabel.textColor = AppDesignUIKit.textPrimary
titleLabel.textAlignment = .center
let uriLabel = UILabel()
uriLabel.text = uri
uriLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
uriLabel.textColor = AppDesignUIKit.textSecondary
uriLabel.textAlignment = .center
uriLabel.numberOfLines = 0
let stack = UIStackView(arrangedSubviews: [icon, titleLabel, uriLabel])
stack.axis = .vertical
stack.spacing = AppMetrics.Spacing.medium
stack.alignment = .center
view.addSubview(stack)
icon.snp.makeConstraints { make in
make.width.height.equalTo(44)
}
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.large)
}
}
}

View File

@ -0,0 +1,107 @@
//
// RemoteImage.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Kingfisher
import UIKit
extension UIImageView {
/// 使 Kingfisher 退
func loadRemoteImage(
urlString: String?,
contentMode: UIView.ContentMode = .scaleAspectFill,
placeholder: UIImage? = nil,
failureImage: UIImage? = nil
) {
let normalized = normalizedURLString(from: urlString)
self.contentMode = contentMode
guard let normalized, let url = URL(string: normalized) else {
kf.cancelDownloadTask()
image = failureImage ?? placeholder
return
}
kf.setImage(
with: url,
placeholder: placeholder,
options: [
.cacheOriginalImage,
.transition(.fade(0.2))
]
) { [weak self] result in
guard let self else { return }
if case .failure = result {
self.image = failureImage ?? placeholder
}
}
}
///
func loadRemoteAvatar(
urlString: String,
systemImageName: String = "person.fill",
iconSize: CGFloat = 44
) {
clipsToBounds = true
layer.cornerRadius = min(bounds.width, bounds.height) / 2
let placeholder = Self.avatarPlaceholder(
systemImageName: systemImageName,
iconSize: iconSize,
size: bounds.size == .zero ? CGSize(width: iconSize, height: iconSize) : bounds.size
)
loadRemoteImage(
urlString: urlString,
contentMode: .scaleAspectFill,
placeholder: placeholder,
failureImage: placeholder
)
}
///
func cancelRemoteImageLoad() {
kf.cancelDownloadTask()
}
private func normalizedURLString(from urlString: String?) -> String? {
let text = urlString?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return text.isEmpty ? nil : text
}
private static func avatarPlaceholder(
systemImageName: String,
iconSize: CGFloat,
size: CGSize
) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
AppDesign.primarySoft.setFill()
context.fill(CGRect(origin: .zero, size: size))
let configuration = UIImage.SymbolConfiguration(pointSize: iconSize, weight: .semibold)
guard let symbol = UIImage(systemName: systemImageName, withConfiguration: configuration)?
.withTintColor(AppDesign.primary, renderingMode: .alwaysOriginal) else {
return
}
let origin = CGPoint(
x: (size.width - symbol.size.width) / 2,
y: (size.height - symbol.size.height) / 2
)
symbol.draw(at: origin)
}
}
}
extension UIImageView {
///
func refreshRemoteAvatarCornerRadius() {
guard bounds.width > 0, bounds.height > 0 else { return }
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
}

View File

@ -0,0 +1,31 @@
//
// UIColor+AppDesign.swift
// suixinkan
//
import UIKit
extension UIColor {
/// 0xRRGGBB UIColor
convenience init(hex: UInt, alpha: CGFloat = 1.0) {
self.init(
red: CGFloat((hex >> 16) & 0xff) / 255.0,
green: CGFloat((hex >> 8) & 0xff) / 255.0,
blue: CGFloat(hex & 0xff) / 255.0,
alpha: alpha
)
}
}
/// UIKit SwiftUI `AppDesign`
enum AppDesignUIKit {
static let primary = UIColor(hex: 0x0073FF)
static let primarySoft = UIColor(hex: 0xEFF6FF)
static let textPrimary = UIColor(hex: 0x1F2937)
static let textSecondary = UIColor(hex: 0x6B7280)
static let placeholder = UIColor(hex: 0xA8B2C1)
static let success = UIColor(hex: 0x14964A)
static let warning = UIColor(hex: 0xFF7B00)
static let pageBackground = UIColor(hex: 0xF5F5F5)
static let cardBackground = UIColor.white
}

View File

@ -0,0 +1,111 @@
//
// ViewControllerHelpers.swift
// suixinkan
//
import SnapKit
import UIKit
/// ViewController ToastLoading ViewModel
@MainActor
enum ViewControllerHelpers {
static var services: AppServices { AppServices.shared }
/// ViewModel onChange deinit
static func bind(onChange: (() -> Void)?, owner: AnyObject, handler: @escaping () -> Void) {
onChange?()
_ = ViewModelBindingToken(owner: owner, handler: handler)
}
}
/// ViewModel onChange
private final class ViewModelBindingToken {
private weak var owner: AnyObject?
private let handler: () -> Void
init(owner: AnyObject, handler: @escaping () -> Void) {
self.owner = owner
self.handler = handler
}
deinit {
handler()
}
}
extension UIViewController {
var appServices: AppServices { AppServices.shared }
func showToast(_ message: String) {
appServices.toastCenter.show(message)
}
func showGlobalLoading(_ message: String = "") {
appServices.globalLoading.show(message: message)
}
func hideGlobalLoading() {
appServices.globalLoading.hide()
}
///
func makeCardView(cornerRadius: CGFloat = 8) -> UIView {
let view = UIView()
view.backgroundColor = AppDesignUIKit.cardBackground
view.layer.cornerRadius = cornerRadius
view.clipsToBounds = true
return view
}
///
func makeEmptyStateView(title: String, message: String, systemImage: String) -> UIView {
let container = UIView()
let stack = UIStackView()
stack.axis = .vertical
stack.alignment = .center
stack.spacing = AppMetrics.Spacing.small
let imageView = UIImageView(image: UIImage(systemName: systemImage))
imageView.tintColor = AppDesignUIKit.textSecondary
imageView.contentMode = .scaleAspectFit
imageView.snp.makeConstraints { make in
make.width.height.equalTo(48)
}
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
titleLabel.textColor = AppDesignUIKit.textPrimary
titleLabel.textAlignment = .center
let messageLabel = UILabel()
messageLabel.text = message
messageLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
messageLabel.textColor = AppDesignUIKit.textSecondary
messageLabel.textAlignment = .center
messageLabel.numberOfLines = 0
stack.addArrangedSubview(imageView)
stack.addArrangedSubview(titleLabel)
stack.addArrangedSubview(messageLabel)
container.addSubview(stack)
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.large)
}
return container
}
///
func makePrimaryButton(title: String) -> UIButton {
var config = UIButton.Configuration.filled()
config.title = title
config.baseBackgroundColor = AppDesignUIKit.primary
config.baseForegroundColor = .white
config.cornerStyle = .medium
config.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16)
let button = UIButton(configuration: config)
return button
}
}