10个值得学习的Swift细节:CardsAgainst自适应字体、NSCache缓存与KVO布局优化清单
2026/8/22 12:27:39 网站建设 项目流程

10个值得学习的Swift细节:CardsAgainst自适应字体、NSCache缓存与KVO布局优化清单

【免费下载链接】CardsAgainstAn iOS game for horrible people项目地址: https://gitcode.com/gh_mirrors/ca/CardsAgainst

CardsAgainst 是一款用 Swift 编写的 iOS 本地组网卡牌游戏,通过 Multipeer Connectivity 让多台设备联机对战。它的代码量很小,却把自适应字体NSCache 缓存KVO 布局优化等实用技巧用到了恰到好处,是新手拆解学习 Swift iOS 开发的优质样例。下面这份清单从源码中提炼了 10 个值得收藏的细节。

项目结构清晰:视图控制器在 View Controllers/,网络与卡牌逻辑在 Controllers/,工具扩展在 Extensions/。

📱 UI 篇:字体缩放与高度缓存

1. 自适应字体:一行比例公式搞定多机型

不同 iPhone 屏幕宽度差异很大,写死的字号在小屏上可能溢出。项目在 UIFont+CardsAgainst.swift 中以 375pt 作为设计基准宽度,按当前屏幕实际宽度等比缩放字号:

private func screenScaledFontSize(_ baseFontSize: CGFloat) -> CGFloat { let screenPortraitWidth = UIScreen.main.nativeBounds.size.width / UIScreen.main.nativeScale return baseFontSize / baseScreenWidth * screenPortraitWidth }

再通过 UIFont 扩展 暴露blackCardFontwhiteCardFont等语义化类变量,全项目调用点简洁统一。黑卡标签还叠加了adjustsFontSizeToFitWidthminimumScaleFactor = 0.5(见 GameViewController.swift),文本过长时自动缩小而不是截断——双重保险。

2. NSCache 缓存 Cell 高度:countLimit 的妙用

Collection View 中每张白卡高度不同,sizeForItemAt若每次都用 Auto Layout 实测,滚动会明显卡顿。项目用 NSCache 以卡片内容的哈希值为键缓存高度,并在 viewDidLoad 中设置countLimit = 20限制条目数:内存吃紧时系统会自动逐出旧值,无需手动清理,也不会引发内存警告

核心流程在 sizeForItemAtIndexPath:先查缓存,命中直接返回;未命中才创建临时 Cell、layoutIfNeeded实测,再把结果写回缓存。注意用setNeedsUpdateConstraints()+updateConstraintsIfNeeded()强制同步布局(cellForItemAt),保证复用时文本立即生效。

3. 现代 KVO API:observe(.bounds) 与生命周期管理

白卡列表的上边距要始终贴着黑卡底边。旧式addObserver需要手动移除,漏掉一行就是崩溃。项目改用 Swift 原生 KVO API:

kvoObserver = blackCardLabel.observe(\.bounds, options: .new) { label, _ in self.whiteCardCollectionView.contentInset = UIEdgeInsets(top: label.frame.size.height + 20 + 64, left: 0, bottom: 20, right: 0) // ... }

关键学习点:观察句柄保存在NSKeyValueObservation?属性中,并在 viewWillDisappear 里invalidate()主动断开,避免闭包持有 self 造成的循环引用——KVO 布局同步的标准姿势

🎬 动效篇:Flow 布局、缓动函数与状态驱动

4. 自定义 FlowLayout:让卡片滑出时优雅淡出

WhiteCardFlowLayout 重写layoutAttributesForElements(in:):当白卡顶部滑入黑卡下方的 120pt "过渡区"时,按距离线性插值计算透明度,实现卡片从黑卡底部浮现的视觉效果。配合 shouldInvalidateLayout(forBoundsChange:) 返回true,每次滚动都重新求值——这是"滚动驱动透明度"动效的经典实现范式。

5. 手写三次缓动函数:easeInOut 的公式拆解

线性插值显得生硬。项目在 WhiteCardFlowLayout.swift 手写了一个三次缓动函数:

private func easeInOut(_ t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) -> CGFloat { var t = t t /= d/2 if t < 1 { return c/2*t*t*t + b } t -= 2 return c/2*(t*t*t + 2) + b }

入参分别是进度、起始值、变化量、时长。把progress喂给easeInOut后再赋给alpha,淡入淡出就有了"先慢后快再慢"的手感。做自定义动效时,这类 10 行以内的小工具函数比引入动画库更划算。

6. didSet 属性观察器:状态一变,UI 自动跟上

hasVoted 用一个Bool属性集中管理"已投票"状态:

fileprivate var hasVoted: Bool = false { didSet { voteButton.tintColor = hasVoted ? lightColor : appTintColor voteButton.isUserInteractionEnabled = !hasVoted scrollViewDidEndDecelerating(scrollView) } }

任何地方只需修改这一个属性,按钮颜色、可交互性、页码同步三处 UI 自动联动,彻底告别"改状态要记得刷 N 个控件"的散弹式修改。这是状态驱动 UI思想的最小可用范例。

🧰 工程篇:延迟加载、洗牌算法与防御式编程

7. 全局 let 懒加载:卡牌数据只加载一次

CardManager.swift 一行private let (blackCards, whiteCards) = loadCards()完成了整副牌的加载。Swift 的全局let常量是惰性初始化且线程安全的:首次访问时才解析 JSON,之后全程复用,无需加锁、无需单例样板代码。数据资源(cards.json)在包内的这种"零样板单例"用法值得记住。

8. 随机抽卡与自动重洗:arc4random 洗牌

takeRandom 的思路很妙:用一副"剩余牌堆",抽完自动sorted { _, _ in arc4random() % 2 == 0 }重洗原牌堆。相比每次randomElement(),它保证了一副牌内不重复,更接近实体游戏的体验,也完全符合卡牌游戏的公平直觉。

9. 启动图按屏高匹配:switch + fatalError 防御

iOS 8 时代需要按屏幕区分启动图。UIImage+LaunchImage.swift 用switch screenHeight按逻辑高度精确匹配 3.5 / 4 / 4.7 / 5.5 英寸与 iPad 各自的启动图;匹配不到就fatalError并附带诊断信息,而不是默默给一张拉伸变形的图。

快速失败 + 明确报错比"能跑但难看"更容易在测试阶段发现问题,这个防御习惯在任何平台通用。

10. 发丝线分隔线与 preferredMaxLayoutWidth

两个一行代码级的细节:

  • 分隔线高度写成 1 / UIScreen.main.scale,在 Retina 屏上正好是一根物理像素的"发丝线",任何设备都不会出现 2pt 粗线。
  • WhiteCardCell 在layoutSubviews里给多行标签设置label.preferredMaxLayoutWidth = label.frame.size.width,让systemLayoutSizeFitting测量换行高度时才准确——这正是第 2 点高度缓存能算对的前提。

多行 UILabel 的自适应高度,十有八九卡在这一个属性上。

✅ 清单回顾

#细节参考位置
1屏宽比例自适应字体UIFont+CardsAgainst.swift
2NSCache 缓存 Cell 高度GameViewController.swift
3KVO 观察 + 生命周期释放GameViewController.swift
4FlowLayout 滚动淡出动效WhiteCardFlowLayout.swift
5手写 easeInOut 缓动函数WhiteCardFlowLayout.swift
6didSet 状态驱动 UIGameViewController.swift
7全局 let 惰性线程安全加载CardManager.swift
8剩余牌堆 + 自动重洗CardManager.swift
9switch 匹配 + fatalError 快速失败UIImage+LaunchImage.swift
10发丝线 & preferredMaxLayoutWidthWhiteCardCell.swift

这套源码不到 2000 行,却覆盖了 SwiftUI 出现之前 UIKit 项目的几乎所有核心功力:字体适配、滚动性能、KVO、自定义布局与动效、防御式编程。建议按清单顺序逐个对照源码走读,每一处都能在半天内吃透并直接迁移到自己的项目里。

【免费下载链接】CardsAgainstAn iOS game for horrible people项目地址: https://gitcode.com/gh_mirrors/ca/CardsAgainst

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询