变暗的 Liquid Glass 和消失的 Apple Intelligence - 肘子的 Swift 周报 #93
安装 iOS 26 beta 3 后,用户会很快发现 Liquid Glass 的效果不再像前两个测试版那样通透。这种在短时间内对 UI 的显著调整再次证明了开发者测试版的价值——让耐受力更高的专业用户率先体验并反馈,对于服务数十亿用户的苹果来说,是至关重要且不可或缺的环节。
安装 iOS 26 beta 3 后,用户会很快发现 Liquid Glass 的效果不再像前两个测试版那样通透。这种在短时间内对 UI 的显著调整再次证明了开发者测试版的价值——让耐受力更高的专业用户率先体验并反馈,对于服务数十亿用户的苹果来说,是至关重要且不可或缺的环节。
AI Coding
依赖插件:
说明: 依赖SourceKitLSP(Language Server Protocol) SourceKit-LSP 是针对 Swift 和基于 C 语言的语言服务器协议(LSP)的实现。它为支持 LSP 的编辑器提供智能编辑器功能,例如代码补全和跳转到定义。
brew install xcode-build-server
SweetPad 是一款 VSCode 扩展,可让您在 VSCode 中构建和运行 iOS、macOS 和 watchOS 应用程序的 Xcode 项目。它基于 Xcode CLI 工具和其他一些开源工具(例如 xcode-build-tools、 xcbeautify、swift-format等)构建。
Kuikly是基于Kotlin MultiPlatform(KMP)构建的跨端开发框架, 支持Android、iOS、HarmonyOS和H5/小程序等多个平台, 据 PCG 开发团队的报告, Kuikly 可以提升团队 85%的开发效率.
这里记录一些我自己使用 Kuikly 遇到的一些坑, 希望后面能少点踩坑吧.
B(l) ARM64 branch out of range (134218124 max is +/-128MB): from
NSString *curUrl = SAFE_CAST([imageView valueForKey:@"css_src"], NSString) ;
然后再跟当前的url比较下./kuiklyw run
执行后, 还要在按一下i
, 切换到 iOS 的热重载, 默认是安卓toInt() / toLong()
, kotlin 非数字字符串 不能转成数字会抛异常. 应该使用 toLongOrNull
, 转不了会返回 null
Switch
组件(系统默认的开关), 连续设置值后, 在 iOS 表现正常, 但是在安卓丢失了背景色导致整个组件都不可见你是否正在为如何备份 iPhone 而烦恼?使用云备份绝对值得考虑。这是保护你移动设备最安全、最便捷的方法之一。云备份的最大优势在于,你可以随时随地访问文件,完全不受限制。继续阅读下文,了解主流云存储服务的使用方法,你会发现将 iPhone 备份到云端其实非常简单快捷。
iCloud 是苹果官方提供的云服务,因此备份 iPhone 时首选就是它。那么如何将 iPhone 文件同步到 iCloud 呢?有两种方式:
将 iPhone 连接到稳定的 Wi-Fi 网络。
打开 iCloud 备份功能:
点击“立即备份”,等待备份完成即可。
解锁 iPhone 并连接至 Wi-Fi。
开启 iCloud 云备份:
Dropbox 是一款广受欢迎的云存储服务,支持备份 iPhone 上的应用、照片、视频、音乐和文档等多种文件类型。操作步骤如下:
OneDrive 是微软提供的云存储服务,支持快速、便捷地存储文档、照片、视频等数据,安全性高。
Google Drive 提供 15GB 免费存储空间,支持备份图片、视频、音频等多种 iPhone 数据。
相比其他云服务,Flickr 虽名气较小,但同样实用。它专注于照片备份,免费空间高达 1TB,可上传数十万张照片。
虽然云服务非常方便,但如果你存储空间有限或网络不稳定,也可以选择将 iPhone 备份到电脑上。最简单的方法是使用 iReaShare iPhone Manager。这款工具操作简单,可将 iPhone 数据快速、安全地传输并备份到 PC 或 Mac,支持超过 10 种数据类型。
步骤1: 下载并安装 iReaShare iPhone Manager,打开软件后用 USB 数据线连接 iPhone 与电脑,按提示信任此电脑。
步骤2: 软件识别设备后,左侧将显示数据分类(如联系人、短信、照片等),点击任意分类可查看详细内容。
步骤3: 勾选你要备份的数据,点击“导出”按钮,即可将文件保存到电脑。
现在你知道如何将 iPhone 备份到云端或电脑了吗?如果已经了解,就赶快行动起来,避免数据丢失的风险。当然,如果你追求更稳定、安全的备份方式,使用 iReaShare iPhone Manager 将 iPhone 备份到电脑会是更好的选择。如在使用过程中遇到问题,欢迎随时联系我们。
本文档结合 CCElectrictyMonitor
和 CCElectricitySampleManager
两个模块的代码,介绍 iOS 应用中电量监控的整体方案、工作流程及统计功能,并附带流程图以便理解。
职责:实时监听设备电量变化,提供代理和闭包两种回调方式,支持 OC 调用。
功能:
UIDevice.current.isBatteryMonitoringEnabled = true
)职责:根据不同运行场景(前台、后台)周期性采集电量数据,统计累计时间,保存采样数据到文件。
功能:
CCElectrictyMonitor
通过定时器定时读取系统电量,通知外部关注者。适合做即时电量变化响应。CCElectricitySampleManager
根据 APP 当前运行场景,周期性采样电量,统计累计耗时和电量变化。适合做长期电量消耗分析。UIDevice.current.batteryLevel
获取当前电量百分比(0~1)Timer.scheduledTimer
每10秒触发一次电量读取minBatteryLevel
@MainActor private func startTimerIfNeeded() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { [weak self] _ in
Task { @MainActor in
self?.observeElectricityChanged()
}
}
observeElectricityChanged()
}
@MainActor @objc public func addSample(scene: EnergySamplingScene,
batteryLevel: Float,
currentMA: Float,
controllerName: String) {
let now = Date().timeIntervalSince1970
guard shouldSample(scene: scene, now: now) else { return }
let startTime = lastSampleTimes[scene] ?? now
let startBattery = lastBatteryLevels[scene] ?? batteryLevel
let duration = now - startTime
totalDuration[scene, default: 0] += duration
let sample = EnergySample(
scene: scene,
startTimestamp: startTime,
endTimestamp: now,
batteryLevelStart: startBattery,
batteryLevelEnd: batteryLevel,
deviceModel: UIDevice.current.modelName,
controllerName: lastControllerNames[scene] ?? controllerName
)
samplesCache.append(sample)
lastSampleTimes[scene] = now
lastBatteryLevels[scene] = batteryLevel
lastControllerNames[scene] = controllerName
if now - lastSaveTime > saveInterval {
saveToFile()
lastSaveTime = now
}
}
// 监听实时电量变化
CCElectrictyMonitor.shared.addElectricityLevelListener { level in
print("当前电量:(level)%")
}
// 在合适时机调用采样(例如根据场景切换)
CCElectricitySampleManager.shared.addSample(scene: .foreground,
batteryLevel: UIDevice.current.batteryLevel,
currentMA: 0.0,
controllerName: "HomeViewController")
// 查询累计时间
let fgDuration = CCElectricitySampleManager.shared.totalDuration(for: .foreground)
print("前台累计时间:(fgDuration)秒")
本方案结合实时电量监听与周期采样统计两部分:
通过该方案,可以科学监控和分析 APP 的电量消耗表现,辅助优化节能策略。
For years, I've been involved in updating LLVM's MC layer. A recentjourney led me to FK_PCRel_
fixup kinds:
MCFixup: Remove FK_PCRel_The generic FK_Data_ fixup kinds handle both absolute and PC-relativefixups. ELFObjectWriter sets IsPCRel to true for `.long foo-.`, so thebackend has to handle PC-relative FK_Data_.However, the existence of FK_PCRel_ encouraged backends to implement itas a separate fixup type, leading to redundant and error-prone code.Removing FK_PCRel_ simplifies the overall fixup mechanism.
As a prerequisite, I had to update several backends that relied onthe now-deleted fixup kinds. It was during this process that somethingunexpected happened. Contributors
To investigate, I downloaded and built GCC 13.3.0 locally:
1 |
../../configure --prefix=$HOME/opt/gcc-13.3.0 --disable-bootstrap --enable-languages=c,c++ --disable-libsanitizer --disable-multilib |
I then built a Release build (-O3
) of LLVM. Sure enough,the failure was reproducible:
1 |
% /tmp/out/custom-gcc-13/bin/llc llvm/test/CodeGen/X86/2008-08-06-RewriterBug.ll -mtriple=i686 -o s -filetype=obj |
Interestingly, a RelWithDebInfo build (-O2 -g
) of LLVMdid not reproduce the failure, suggesting either an undefined behavior,or an optimization-related issue within GCC 13.3.0.
I built GCC at the releases/gcc-13
branch, and the issuevanished. This strongly indicated that the problem lay somewhere betweenthe releases/gcc-13.3.0
tag and thereleases/gcc-13
branch.
The bisection led me to a specific commit, directing me to
I developed a workaround at the code block with a typo "RemaningOps".Although I had observed it before, I was hesitant to introduce a commitsolely for a typo fix. However, it became clear this was the perfectopportunity to address both the typo and implement a workaround for theGCC miscompilation. This led to the landing of
Sam James from Gentoo mentioned that the miscompilation wasintroduced by a commit cherry-picked into GCC 13.3.0. GCC 13.2.0 and GCC13.4.0 are good.
这里每天分享一个 iOS 的新知识,快来关注我吧
在科技领域,苹果作为创新的先锋,其每一步动向都备受瞩目。
我们都知道,苹果手机在海外打开浏览器,默认使用 Google 搜索,在国内默认使用百度(也可以自己设置)。但大家有没有想过,苹果作为世界上最大的手机制造商,为什么不做自己的搜索引擎?
近期苹果高级副总裁艾迪·库(Eddy Cue)在美国华盛顿特区的联邦法院提交了一份声明,解释了为何苹果不打算像谷歌一样开发自己的搜索引擎。
看完报告之后才发现原来这一决定背后有着深刻的考量。
艾迪·库在声明中指出,苹果反对开发搜索引擎的原因主要有以下几点:
成本与资源投入
研发一个搜索引擎将花费苹果数十亿美元,并且需要很多年时间。这将会使得苹果不得不将投资资金和员工从公司目前专注的其他增长领域中抽离。
今年苹果刚刚放弃计划 10 年的汽车项目,转而投入 AI,看起来已经没有更多足够的精力去专心做一个搜索引擎了。
快速变化的搜索行业
随着人工智能的迅猛发展,搜索业务正处于快速演变之中。在此背景下,苹果认为建立一个搜索引擎在经济上存在巨大风险。
其实这背后预示着,即使像 Google 这样的大公司,面对 AI 搜索的步步紧逼,其未来也不光明。如果不能把握住 AI,其主营的业务搜索也可能被 OpenAI 这样的新兴 AI 公司代替。
广告销售与隐私承诺
为了创建一个“可行”的搜索引擎业务,苹果将不得不“销售定向广告”。但这并不是苹果的核心业务,而且会违背公司长久以来对用户隐私的承诺。
苹果一直以来致力于用户隐私的保护,如果做搜索引擎,则可能会因为需要盈利而背离这一原则。
专业人员与运营基础设施的缺乏
苹果目前没有足够的“专业化人才”和“运营基础设施”来构建和运营一个成功的搜索引擎业务。
前面提到了在海外,苹果手机默认搜索引擎是谷歌,因为这项合作,Google 每年要向苹果支付 200 亿美元,因此即使自己不做搜索引擎,苹果也能在这方面获得丰厚的利润,因此也就没有动力自己做一套全新的搜索引擎了。
在今年上半年的时候,美国司法部针对谷歌的反垄断审判中,法院裁定谷歌与苹果达成的将谷歌设为 Safari 默认搜索引擎的协议是非法的。对此,艾迪·库在声明中请求法院允许苹果在审判中通过自己的证人作证来为这项协议辩护。
艾迪·库在声明中写道:“只有苹果能够说明哪些未来的合作可以最好地服务于其用户。”他强调,苹果始终专注于创造最佳的用户体验,并通过与其他公司的潜在合作和安排来实现这一目标。
艾迪·库还透露,仅在 2022 年,谷歌就向苹果支付了大约 200 亿美元作为协议的一部分。如果这一协议无法继续,库表示:“这将严重阻碍苹果继续提供最符合用户需求产品的能力。”
不过话说回来,如果最终法院裁定苹果不能再将谷歌设为 Safari 默认搜索引擎,那我觉得苹果还是有很大概率自己做一套搜索引擎的。
苹果拒绝创建自己的搜索引擎,虽然有经济上的考量,更深层次的原因在于苹果对用户隐私的坚定承诺和对核心业务的专注。
这样的决策不仅反映了苹果的战略思维,也显示了其对用户体验的重视。如果你对苹果的这项决定有什么看法,欢迎在评论区分享你的观点。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
swift中编码与解码有两个协议,Encodable和Decodable。
下面是编码与解码的示例
import Foundation
// 编码
struct Person: Encodable {
let name: String
let age: Int
}
let person = Person(name: "lihua", age: 18)
let encodedData = try JSONEncoder().encode(person)
if let jsonsString = String(data: encodedData, encoding: .utf8){
print(jsonsString)
}
需要解释的是 编码通过JSONEncoder()编码器将Swift对象转为json格式,encode(person)将对象编码,但只是一种抽象,具体编码成什么,还需要看是什么编码器。
这里的二进制json数据不方便查看,所以需要转为字符串。
//解码
struct Education: Decodable {
let classname: String
let school: String
}
let jsonString = """
{
"classname": "one",
"school": "first"
}
"""
if let jsonData = jsonString.data(using: .utf8){
let education = try JSONDecoder().decode(Education.self, from: jsonData)
print(education.classname)
} else{
print("解码失败")
}
这里我们将字符串的jsonString转为json,由于data方法返回的是一个可选型,所以需要可选绑定。这里使用if let
解码把data数据转换为Education的实例,这里decode方法的第一个参数是一个类型,返回一个实例对象,所以写成decode(Education.self, from: jsonData),Education.self表示类型本身,而Education表示对象
这个协议是解码和编码的组合
//codable
import Foundation
// ✅ 定义模型
struct Person2: Codable {
let name: String
let age: Int
}
// ✅ 模拟后端返回的 JSON 字符串
let jsonString2 = """
{
"name": "Tom",
"age": 25
}
"""
// ✅ 模拟网络返回的二进制 Data
if let jsonData = jsonString2.data(using: .utf8) {
do {
// ✅ JSONDecoder 解码 Data → Person 对象
let person = try JSONDecoder().decode(Person2.self, from: jsonData)
// ✅ 使用解析出的对象
print("名字:\(person.name)")
print("年龄:\(person.age)")
// ✅ 编码示例(Swift 对象 → JSON)
let encodedData = try JSONEncoder().encode(person)
if let jsonString = String(data: encodedData, encoding: .utf8) {
print("编码后的 JSON 字符串:\(jsonString)")
}
} catch {
print("解析或编码失败:\(error)")
}
}
我们来看一看解码内部,decode遵循Decodable协议,返回一个实例对象
Decodable和Encodable本身没有什么好说的,就是把对象编码成其他格式和把其他格式解码并初始化一个实例对象的能力,里面有方法encode和decode,重要的是其中的方法遵循的协议。Eocoder和Decoder。解码时,会生成一个实例对象,对象的参数必须初始化,所以使用init()。
下面是Encoder协议
public protocol Encoder {
/// The path of coding keys taken to get to this point in encoding.
var codingPath: [any CodingKey] { get }
/// Any contextual information set by the user for encoding.
var userInfo: [CodingUserInfoKey : Any] { get }
/// Returns an encoding container appropriate for holding multiple values
/// keyed by the given key type.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `unkeyedContainer()` or after
/// encoding a value through a call to `singleValueContainer()`
///
/// - parameter type: The key type to use for the container.
/// - returns: A new keyed encoding container.
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
/// Returns an encoding container appropriate for holding multiple unkeyed
/// values.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `container(keyedBy:)` or after
/// encoding a value through a call to `singleValueContainer()`
///
/// - returns: A new empty unkeyed container.
func unkeyedContainer() -> any UnkeyedEncodingContainer
/// Returns an encoding container appropriate for holding a single primitive
/// value.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `unkeyedContainer()` or
/// `container(keyedBy:)`, or after encoding a value through a call to
/// `singleValueContainer()`
///
/// - returns: A new empty single value container.
func singleValueContainer() -> any SingleValueEncodingContainer
}
协议有三种方法,定义了三种容器,即按照编码的类型不同,放入不同的容器中,container适合键值对的数据,unkeyedContainer适合无键的数据,即数组,singleValueContainer适合单个的数据。键值对数据要遵守CodingKey协议,这是什么?
由源码可以知道,它从对象属性得到key,映射对象属性和最终外部数据之间的对应关系。
事情还没有完,得到容器类型后呢?看看第一种,
它遵循了一个协议,协议下面有编码方法encode,就得到一个想要的类型,
一个完整的编码步骤应该是这样:
import Foundation
struct User: Encodable {
var fullName: String
var age: Int
var email: String
// 定义自定义 CodingKey
enum CodingKeys: String, CodingKey {
case name // fullName 映射到 "name"
case age
case email
}
func encode(to encoder: Encoder) throws {
// 获取 Keyed container
var container = encoder.container(keyedBy: CodingKeys.self)
// 手动映射属性到 key,并进行自定义逻辑
try container.encode(fullName, forKey: .name)
// 自定义处理,比如把年龄「加密」后再存
let encryptedAge = age + 100
try container.encode(encryptedAge, forKey: .age)
try container.encode(email, forKey: .email)//常规
}
}
你已经看到,我在编码时手动实现了一些东西,这是它的优势,虽然代码量大了 我可以修改:
struct User: Encodable {
var fullName: String
enum CodingKeys: String, CodingKey {
case name // JSON 需要的 key
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(fullName, forKey: .name)
}
}
比如你想对涉密字段加密
try container.encode(phoneNumber.masked(), forKey: .phone)
// 不编码某些属性(不写就不会被编码)
try container.encode(name, forKey: .name)
// 不调用 encode(password, forKey: .password),密码就不会被写入
let fullName = "\(firstName) \(lastName)"
try container.encode(fullName, forKey: .fullName)
if isPremiumUser {
try container.encode(premiumInfo, forKey: .premiumInfo)
}
只有会员才编码写入json
解码也类似:
struct User: Decodable {
var fullName: String
var age: Int
var email: String
// 定义自定义 CodingKey
enum CodingKeys: String, CodingKey {
case name
case age
case email
}
// ✅ 自定义解码逻辑
init(from decoder: Decoder) throws {
// 获取 Keyed container
let container = try decoder.container(keyedBy: CodingKeys.self)
// 这里可以进行复杂映射或校验
// 例如:把 JSON 里的 name 映射到 fullName
self.fullName = try container.decode(String.self, forKey: .name)
// 自定义处理:从「加密」后的 age 解密
let encryptedAge = try container.decode(Int.self, forKey: .age)
self.age = encryptedAge - 100
// 常规解码 email
self.email = try container.decode(String.self, forKey: .email)
}
}
崇祯年间,华山派武学虽盛,却在应对江湖新局时渐显颓势;如今 SwiftUI 江湖亦是如此 ——WWDC 25 之前,若要在 SwiftUI 中显示网页,开发者恰似袁承志初闯江湖,纵有一身本领,却苦无称手兵刃。
直到那柄 "金蛇剑" 般的全新 WebView 横空出世,才让网页显示之道豁然开朗。
在本篇武学大家谈中,各位少侠将学到如下内容:
想得到那柄可以横扫武林的神兵利器金蛇剑吗?那还等什么?让我们马上开始寻“剑”之旅吧!
Let's go!!!;)
想当年,SwiftUI 自身并无网页显示的独门心法,开发者们只得借 UIKit 的 WKWebView 这柄 "钝剑",再辅以UIViewRepresentable
为鞘,方能勉强施展。
这般操作,犹如袁承志在华山练剑时,需先扎三年马步 —— 基础虽牢,却失之滞涩。
且看这套 "华山入门剑法":
import SwiftUI
import WebKit
// 以UIViewRepresentable为桥,连接SwiftUI与WKWebView
struct WebViewWrapper: UIViewRepresentable {
let url: URL?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard let url = url else { return }
uiView.load(URLRequest(url: url))
}
}
// 实战时需如此调用,恰似执钝剑闯敌营
struct ContentView: View {
var body: some View {
WebViewWrapper(url: URL(string: "https://apple.com"))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
这套功夫虽能御敌,却有三大弊端:
updateUIView
反复调用时易生错乱,好比剑法中 "剑招互碍";正当开发者们困于旧法之时,WWDC 25 恰似一场武林大会,苹果突然亮出 "金蛇剑"——SwiftUI 原生 WebView 横空出世!此剑一出,如金蛇郎君夏雪宜重现江湖,招式灵动,浑然天成,将网页显示之道推向新境。
新 WebView 的基础用法,恰似袁承志初得金蛇剑时的随手一挥,看似简单却暗藏玄机:
import SwiftUI
import WebKit
struct ContentView: View {
// 直接使用URL初始化,无需繁琐包装
var body: some View {
WebView(url: URL(string: "https://apple.com"))
.navigationTitle("金蛇洞")
.edgesIgnoringSafeArea(.bottom)
}
}
这般代码,较之旧法省去近八成冗余,正如金蛇剑法 "险、奇、快" 之妙 —— 无需再写UIViewRepresentable
的桥接代码,无需手动管理 WebView 生命周期,SwiftUI 自会料理妥当。
真乃呜呼快哉!
若要深入掌控网页状态,需修习 "金蛇秘籍"——WebPage
类。此物如同袁承志从山洞中所得的金蛇锥谱,将网页的标题、URL、加载进度等信息尽收其中:
import SwiftUI
import WebKit
internal import Combine
// 实战运用:将心法与招式结合
struct ContentView: View {
@State private var page = WebPage()
@State private var id: WebPage.NavigationID?
@State private var isLoading = false
@State private var event: WebPage.NavigationEvent?
var body: some View {
NavigationStack {
WebView(page)
.navigationTitle(page.title)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
page.reload()
}) {
Image(systemName: "arrow.counterclockwise")
}
}
ToolbarItem(placement: .topBarLeading) {
if isLoading {
ProgressView()
}
}
}
.onChange(of: page.isLoading) { _, new in
isLoading = new
}
.onReceive(page.currentNavigationEvent.publisher) { event in
guard event.navigationID == id else { return }
switch event.kind {
case let .failed(underlyingError: error):
print(error.localizedDescription)
case .finished:
print("网页加载完毕")
default:
break
}
}
.task {
let request = URLRequest(url: .init(string: "https://blog.csdn.net/mydo")!)
id = page.load(request)
}
}
}
}
此处关键在于@Published
属性 —— 当网页标题变化时,导航栏会如响应内力般自动更新;进度条则像金蛇吐信般实时菊花旋转:
这般状态同步,较旧法中手动绑定NotificationCenter
的操作,可谓 "化繁为简,返璞归真"。
新 WebView 最令人称道之处,莫过于对 JavaScript 交互的 "化骨绵掌" 式处理。
昔日要在 Swift 与 JS 间传递数据,需写十数行桥接代码,如同袁承志在华山与温家五老缠斗时的狼狈;如今却能一剑破局:
// 实战运用:将心法与招式结合
struct ContentView: View {
@State private var page = WebPage()
@State private var id: WebPage.NavigationID?
@State private var isLoading = false
@State private var event: WebPage.NavigationEvent?
@State private var titleFromJS: String?
var body: some View {
NavigationStack {
WebView(page)
.navigationTitle(page.title)
.toolbar {
ToolbarItemGroup {
Button {
Task {
if let jsResult = try? await page.callJavaScript(
"""
return document.title;
"""
), let title = jsResult as? String {
titleFromJS = title
}
}
} label: {
Image(systemName: "figure.run")
}
Button {
Task {
try? await page.callJavaScript(
"""
document.body.style.backgroundColor = 'gold'
"""
)
}
} label: {
Image(systemName: "figure.cricket")
}
Button(action: {
page.reload()
}) {
Image(systemName: "arrow.counterclockwise")
}
}
ToolbarItem(placement: .topBarLeading) {
if isLoading {
ProgressView()
}
}
}
.onChange(of: page.isLoading) { _, new in
isLoading = new
}
.onReceive(page.currentNavigationEvent.publisher) { event in
guard event.navigationID == id else { return }
switch event.kind {
case let .failed(underlyingError: error):
print(error.localizedDescription)
case .finished:
print("网页加载完毕")
default:
break
}
}
.task {
let request = URLRequest(url: .init(string: "https://blog.csdn.net/mydo")!)
id = page.load(request)
}
.safeAreaInset(edge: .top) {
if let title = titleFromJS {
Text(title)
.font(.title2)
.padding()
.background(.thinMaterial.opacity(0.66), in: RoundedRectangle(cornerRadius: 15))
}
}
}
}
}
在上面的代码中,我们用 JavaScript 做了两件事:
此等操作,恰似金蛇郎君以金蛇锥破敌甲胄 —— 直接穿透 Swift 与 JS 的壁垒。
更妙者,新 WebView 对各平台特性的适配,如 visionOS 的 "看向滚动" 功能,只需一行修饰符即可大功告成:
WebView(webView: page.webView)
#if os(VisionOS)
// 开启 VisionOS 滚动输入”通天眼“
.webViewScrollInputBehavior(.enabled, for: .look)
#endif
.scrollBounceBehavior(.basedOnSize) // 滚动反馈如"踏雪无痕"
回首 SwiftUI 的网页显示之道,恰似袁承志的武学进阶:从华山派的循规蹈矩,到金蛇剑法的灵动不羁,再到融会贯通自成一派。
WWDC 25 推出的新 WebView,不仅解决了旧法中的 "招式沉冗" 之弊,更将 SwiftUI 的声明式编程理念推向新高度。
正如金蛇剑在袁承志手中终成一代传奇,这套新 WebView API 亦将成为开发者闯荡网页江湖的不二之选。
毕竟,真正的好功夫,从来都是 "大道至简,大巧若拙"—— 能以三两行代码办妥之事,何必耗费十数行力气?此乃 WWDC 25 留给 SwiftUI 开发者的最大启示,亦是江湖不变之真理。
那么,各位少侠看到这里又作何感想呢?
感谢宝子们的观看,再会啦!8-)
你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。
Swift 官方宣布成立 Android 工作组,将 Android 列为官方支持的平台。该工作组的主要目标是为 Swift 语言添加并维护 Android 平台支持,让开发者能够使用 Swift 开发 Android 应用。
@Barney:这篇文章是关于 Apple's FoundationModels 在 Swift 开发中的提示工程指南。Apple 的 Foundation Model 专门为 Swift 和 SwiftUI 训练,有 4096 token 的上下文限制。核心技术是使用 Generable 宏定义输出结构,通过 Guide 系统精确控制生成内容。文章强调属性顺序的重要性,因为 LLM 逐 token 生成。实用技巧包括自然语言长度修饰符、角色设定、少样本提示法和温度调节。对于用户输入,建议限制开放字段并妥善处理 guardrail 错误。为 Swift 开发者提供了原生、类型安全的 AI 集成方案。
@Kyle-Ye: 独立 iOS 开发者 David Smith 分享了他在基于 iOS 26 设计语言重新设计 Pedometer++ 的地图功能的一些思考。文章详细描述了他的设计过程,包括如何让地图全屏显示、如何优化顶部按钮、如何设计浮动的信息面板等。
@Cooper Chen:在 AI 编程工具被广泛吹捧的背景下,METR 实验室通过一项严谨的随机对照试验(RCT)揭示了一个反直觉的结论:经验丰富的开发者在成熟项目中使用 AI 工具后,工作效率反而降低 19%。这项研究基于 16 位资深开源开发者在百万行代码项目中的 246 项真实任务,挑战了“ AI 必然提升效率”的行业共识。
关键发现:
@阿权:文章详细介绍了如何使用 WWDC25 推出的 AlarmKit 框架实现倒计时提醒功能。过去要实现指定时间提醒功能,普通开发者只能通过苹果的通知推送。虽然通知能自定义时机甚至提醒铃声,但始终还是通知,在静音模式和专注模式下都无一幸免,要想像系统闹钟一样即使在静音和专注模式下还能提醒,只能通过新推出的 AlarmKit 了。AlarmKit 支持一次性闹钟、重复闹钟和立即开始的倒计时提醒,AlarmKit 提供的能力需要用户授权,并需要适配锁屏展示和灵动岛中的展示,具体配置可浏览原文。
只希望该功能不要被厂商滥用,尤其不要用在“加急”功能上啊!
@david-clang:本文的分享更侧重于科普类型的概括,包括 Flutter 的市场渗透率、技术进展、未来方向,其中有几个有趣的点:
市场渗透率:
技术进展:
未来方向:
@DylanYang:使用 Xcode 26 构建包,跑在版本号小于 iOS 26 的系统上会在启动阶段遇到设备启动崩溃 Symbol not found: NSUserActivityTypeBrowsingWeb。原因是 CoreServices 在 iOS26 SDK 中重新导出了 NSUserActivityTypeBrowsingWeb 符号,导致链接时将符号绑定到了 CoreServices 模块。修复方案是把 Foundation 的在链接参数中的位置往前面提到 CoreServices 之前。
CrazyFanFan 提供信息
重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考
具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)
我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参
同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom 。
🚧 表示需某工具,🌟 表示编辑推荐
预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)
最近我在 bgg 上闲逛时了解到了“Make-as-You-Play”这个游戏子类型,感觉非常有趣。它是一种用纸笔 DIY (或叫 PnP Print and Play)的游戏,但又和传统 DIY 游戏不同,并不是一开始把游戏做好然后再玩,而是边做边玩。对于前者,大多数优秀的 PnP 都有专业发行商发行,如果想玩可以买一套精美的制成品;但 Make as You Play 不同,做的过程是无法取代的,做游戏就是玩的一部分。
深度未来 Deep Future 是“做即是玩”类型的代表作。它太经典了,以至于有非常多的玩家变体、换皮重制。我玩的官方 1.6 版规则。btw ,作者在 bgg 上很活跃,我在官方论坛八年前的规则讨论贴上问了个规则细节:战斗阶段是否可以不损耗人口“假打”而只是为了获得额外加成效果。作者立刻就回复了,并表示会在未来的 1.7 规则书上澄清这一点。
读规则书的确需要一点时间,但理解了游戏设计精神后,规则其实都很自然,所以游戏进程会很流畅。不过依然有许多细节分散在规则书各处,只有在玩过之后才会注意到。我(单人)玩了两个整天,大约玩了接近 100 局,酣畅淋漓。整个游戏的过程有如一部太空歌剧般深深的刻印在我的脑海里,出生就灭亡的文明、离胜利只有一步之遥的遗憾、兴起衰落、各种死法颇有 RogueLike 游戏的精神。难怪有玩家会经年玩一场战役,为只属于自己战役的科技和文明设计精美的卡片。
在玩错了很多规则细节后,我的第一场战役膨胀到了初始卡组的两倍,而我也似乎还无法顺利胜利哪怕一局。所以我决定重开一盒游戏。新的战役只用了 5 盘就让银河推进到了第二纪元(胜利一次),并在地图上留下了永久印记,并制作了第一张文明卡片。这些会深刻的影响同场战役的后续游戏进程。
我感觉这就是这类游戏的亮点:每场游戏都是独特的。玩的时间越长,当前游戏宇宙的特点就有越来越深刻的理解:宇宙中有什么特别的星球、科技、地图的每个区域有不同的宜居星球密度,哪里的战斗强度会更大一些…… 虽然我只玩了单人模式,但游戏支持最多三人。多人游戏可以协作也可以对抗。你可以邀请朋友偶尔光临你的宇宙玩上两盘,不同的玩家会为同一个宇宙留下不同的遗产。和很多遗产类游戏不同,这个游戏只要玩几乎一定会留下点什么,留不下遗产的游戏局是及其罕见的。也就是说,只要玩下去哪怕一小盘都会将游戏无法逆转的改变。
下面先去掉细节,概述一下游戏规则:
游戏风格类似太空版文明,以一张六边形作为战场。这是边长为 4 的蜂巢地图(类似扩大一圈的卡坦岛),除去无法放置人口方块的中心黑洞,一共是 36 个六边形区格。玩家在以一个母星系及三人口开局,执行若干轮次在棋盘上行动。可用行动非常类似 4X 游戏:生产、探索、繁殖、发展、进攻、殖民。
每个玩家有 4 个进度条:文化 C、力量 M 、稳定 S 、外星 X。除去文化条外,其余三个条从中间开始,一旦任意一条落到底就会失败;而任意一条推进到顶将可能赢得游戏。文化条是从最底部开始,它推进到顶(达成文化胜利)需要更多步数,但没有文化失败。
另外,控制 12 个区域可获得疆域胜利,繁殖 25 个人口可获得人口胜利。失去所有星球也会导致失败。在多人模式中,先失败的玩家可以选择在下个回合直接在当前游戏局重新开始和未失败的玩家继续游戏(但初始条件有可能比全新局稍弱)。
游戏以纯卡牌驱动,每张卡片既是行动卡,又是系统事件卡,同时卡片还是随机性的来源。抽取卡片用于产生随机性的点数分布随着游戏发展是变化的,每场战役都会向不同的方向发展,这比一般的骰子游戏的稳定随机分布会多一些独有的乐趣。
玩家每轮游戏可作最多两个独立的行动:
在执行这些行动的同时,如果玩家拥有更多的科技,就可能有更多的行动附加效果。这些科技带来的效果几乎是推进胜利进度条的全部方法,否则只有和平状态的 BATTLE 行动才能推进一格进度条。
在行动阶段之后,系统会根据玩家帝国中科技卡的数量产生不同数量的负面事件卡。科技卡越多,面临的挑战越大。但可以用手牌支付科技的维护费来阻止科技带来的额外负面事件,或用手牌兑换成商品寄存在母星和科技卡上供未来消除负面事件使用。
负面事件卡可能降低玩家的胜利进度条,最终导致游戏失败;也可能在地图增加新的野生星球及野怪。后者可能最终导致玩家失去已殖民的星球。但足够丰富的手牌以及前面用手牌制造的商品和更多的殖民星球可以用来取消这些负面事件。
每张星球卡和科技卡上都有三个空的科技栏位,在生成卡片时至少会添加一条随机科技,而另两条科技会随着游戏进程逐步写上去。
游戏达成胜利的必要条件是玩家把母星的三条科技开发完,并拥有至少三张完成的科技卡(三条科技全开发完毕),然后再满足上面提到的 6 种胜利方式条件之一:四个胜利进度条 C T S X 至少一条推进到顶,或拥有 12 区域,亦或拥有 25 人口。
胜利的玩家将给当局游戏的母星所在格命名,还有可能创造 wonder ,这会影响后面游戏的开局设定。同时还会根据这局游戏的胜利模式以及取得的科技情况创造出一张新的文明卡供后续游戏使用。
游戏以 36 张空白卡片开始。一共有 6 种需要打出卡片的行动,(EVOKE 和 PLAN 不需要行动卡),每种行动在6 张空白卡上画上角标 1-6 及行动花色。太阳表示 POWER ,月亮表示 SETTLE ,爱心表示 GROW ,骷髅表示 ADVANCE ,手掌表示 BATTLE ,鞋子表示 EXPAND 。这些花色表示卡片在手牌上的行动功能,也可以用来表示负面事件卡所触发的负面事件类别(规则书上有一张事件查阅表)。
数字主要用来生成随机数:比如在生成科技时可以抽一张卡片决定生成每个类别科技中的 6 种科技中的哪一个(规则书上有一张科技查阅表),生成随机地点时则抽两张组成一个 1-36 的随机数。
我初玩的时候搞错了一些规则细节,或是对一些规则有疑惑,反复查阅规则书才确定。
开局的 12 个初始设定星球是从 36 张初始卡片中随机抽取的卡片随机生成的,而不是额外制作 12 张卡片。
如果是多人游戏,需要保证每个玩家的母星上的初始科技数量相同。以最多科技的母星为准,其余玩家自己补齐科技数量。无论是星球卡还是科技卡,三个科技的花色(即科技类别)一定是随机生成的。这个随机性通过抽一张卡片看角标的花色决定。通常具体科技还需要再抽一张卡,通过角标数字随机选择该类别下的特定科技。
每局游戏的 Setup 阶段,如果多个野生星球生成在同一格,野怪上限堆满 5 个即可,不需要外溢。但在游戏过程中由负面事件刷出来的新星球带来的野怪,放满格子 5 个上限后,额外的都需要执行外溢操作:即再抽一张卡,根据 1-6 的数字决定放在该格邻接的 6 格中的哪一格,从顶上面邻格逆时针数。放到版图外面的可以弃掉,如果新放置的格也慢了,需要以新的那格为基准重复这个操作,直到放完规定数量。放在中心黑洞的野怪暂时放在那里,直到所有负面事件执行外,下一个玩家开始前再弃掉。
开始 START 阶段,玩家是补齐 5 张手牌,如果超过 5 张则不能抽牌但也不需要丢到 5 张。超过 10 张手牌则需要丢弃多余的牌。是随机丢牌,不可自选。
殖民星球的 START 科技也可以在开始阶段触发且不必丢掉殖民星球。但在行动阶段如果要使用殖民星球的科技,则是一次性使用,即触发殖民星球上的科技就需要弃掉该星球。
在 START 阶段触发的 Explorarion 科技可以移动一个 cube 。但它并不是 EXPAND 行为,所以不会触发 EXPAND 相关科技(比如 FTL),也无法获得 Wonder 。和 EXPAND 不同,它可以移动区域中唯一的一个 cube ,但是失去控制的区域中如果有殖民星球,需要从桌面弃掉。
玩家不必执行完两个行动、甚至一个行动都不执行也可以。不做满两个行动在行动规划中非常普遍。
PLAN 行动会立刻结束行动阶段,即使它是第一个行动。所以不能利用 PLAN 制造出来的卡牌在同一回合再行动。
行动的科技增益是可选发动的。同名的科技也可以叠加。母星和桌面的科技卡上提供的科技增益是无损的,但殖民星球和手上的完整科技卡提供的科技是一次性的,用完就需要弃掉。
完成了三项科技的科技卡被称作完整科技卡,才可以在当前游戏中当手牌使用。不完整科技卡是不能当作手牌提供科技增益的。
SETTLE 行动必须满足全部条件才可以发动。这些条件包括,你必须控制想殖民的区域(至少有一个人口在那个格子);手上需要有这个格子对应的星球卡或该星球作为野生星球卡摆在桌面。手上没有对应格的星球卡时,想殖民必须没有任何其它星球卡才可以。这种情况下,手上有空白卡片必须用来创造一张新的星球卡用于殖民,只有没有空白卡时,才创造一张全新的星球卡。多人游戏时,创造新的星球卡的同时必须展示所有手牌以证明自己没有违反规则。如果殖民的星球卡是从手牌打出,记得在打出星球卡后立刻抽一张牌。新抽的牌如果是完整科技卡也可以立刻使用。如果星球卡是新创造的,或是版图上的,则不抽卡。
SETTLE 版图上的野生星球的会获得一个免费的 POWER 行动和一个免费的 ADVANCE 行动。所谓免费指不需要打出行动手牌,也不占用该回合的行动次数。这视为攻打野生星球的收益,该收益非常有价值,但它是可选的,你也可以选择不执行
SETTLE 的 Society 科技增益可以让玩家无视规则限制殖民一个星球。即不再受“手牌中没有其它可殖民星球”这条限制,所以玩家不必因此展示手牌。使用 Society 科技额外殖民的星球总是可以选择使用手上的空白卡或创造一张新卡。这个科技不可堆叠,每个行动永远只能且必须殖民一个星球。
SETTLE 的 Goverment 科技增益可以叠加,叠加时可以向一科技星球(星球卡创建时至少有一科技)添加两个科技,此时玩家先添加两个随机花色,然后可以圈出其中一个选择指定科技,而不需要随机选择。
GROW 的 Biology 科技增益必须向不同的格子加人口,叠加时也需要每个人口都放在不同格。如果所控区域太少,可能浪费掉这些增益。
如果因为人口上限而一个人口也无法增加,GROW 行动无法发动。所以不能打出 GROW 卡不增加人口只为了获得相关科技增益。
未完成的科技卡在手牌中没有额外功能。它只会在 ADVANCE 行动中被翻出并添加科技直到完成。如果 ADVANCE 时没有翻出空白卡或未完成的科技卡,则创造一张新科技卡。新创建的科技卡会立刻随机生成三个随机花色。玩家可以选择其中一个花色再随机出具体科技。在向未完成的科技卡上添加新科技时,如果卡上没有圈,玩家可以选择圈出一个花色自主选择科技,而不必随机。一张卡上如果圈过,则不可以再自主选择。
ADVANCE 的 Chemistry 科技增益可以重选一次随机抽卡,可以针对花色选择也可以针对数字选择。但一个 Chemistry 只能重选一次,这个科技可以叠加。
ADVANVE 的 Physics 科技增益只能在添加科技到科技卡时才能生效。不能针对星球卡添加科技时向另一张科技卡使用。所以,无论 Physics 叠加与否,都最多向科技卡添加两条科技(因为科技卡一定会至少先生成一条)。当 Physics 叠加两次时(三次叠加没有意义),玩家可以自主选择新加的两条科技(向一科技卡添加原本就可以有一条自由选择权,叠加 Physics 增加了一次选择权)。注意,花色一定是随机生成的。
只有在所有邻接格都没有敌人(野怪和其他玩家)时,才可以发动 BATTLE 行动的推进任意胜利条的功能。战斗默认是移除自己的人口,再移除敌人相同数量的人口。但可以选择移除自己 0 人口来仅仅发动对应增益。所以 BATTLE 行动永远都是可选的。
BATTLE 的 Military 科技增益新增的战场可以重叠,即可以从同一己方格攻打不同敌人格,也可以从多个己方格攻打同一敌人格。和 Defence 科技增益同时生效时,可以一并结算。
EXPAND 行动必须移动到空格或己方控制格,但目的地不可以超过 5 人口上限。永远不会在同一格中出现多个颜色的人口。移动必须在出发地保留至少一个人口。当永远 FTL 科技增益时,可以移动多格,途经的格不必是空格,也可以是中心黑洞。
EXPAND 行动移动到有 Wonder (过去游戏留下来的遗产)的格子,且该格为空时,可以通过弃掉对应花色的手牌发动 Wonder 能力,其威力为弃牌的角标数字。Wonder 只能通过 EXPAND 触发,不会因为开局母星坐在 Wonder 格触发。
BATTLE 的 spaceship 科技增益需要选择不同的目的地,多个叠加也需要保证每个目的地都不相同。
PLAN 行动制造新卡时,只有花色是自选的,数字还是随机的。PLAN 会结束所有行动。
行动阶段后的 Payment 阶段可以用来消除之后 Challenge 阶段的负面事件数量。方法是打出和母星及科技卡上科技增益的花色。针对母星以及每张科技卡分别打出一张。如果卡片上有多个科技增益花色,任选其中一个即可。科技卡上未填上的增益对应的花色则不算。每抵消一张就可以减少一张事件卡,但事件卡最后至少会加一张。每次抵消一次事件,都可以所在卡片(母星或科技卡)上添加一个 upkeep 方块。每张卡上的方块上限为 3 ,不用掉就不再增加。但到达上限后,玩家依旧可以用手牌抵消事件,只不过不再增加方块。
挑战阶段,一张张事件卡翻开。玩家可以用对应花色的手牌取消事件,也可以使用桌面方块取消,只需要方块所在卡片上有同样花色。还可以使用殖民星球取消,需要该星球上有对应花色的科技(不是星球卡的角标花色)。但使用殖民星球需要弃掉该星球卡。不可使用母星抵消事件卡。
事件生效时,如果需要向版图添加野怪。这通常是增加随机方块事件,和增加野外星球事件(带有 5 方块)。增加的方块如果在目标格溢出,需要按规则随机加在四周。
如果增加的方块所在格有玩家的方块,需要先一对一消除,即每个增加的野怪先抵消掉一个玩家方块。如果玩家因此失去一个区域,该区域对应的桌面星球也需要扔掉,同时扔掉牌上面的方块。如果母星因此移除,玩家可以把任意殖民星球作为新的母星。移除的母星会变成新的野外星球。如果玩家因此失去所有星球就会失败。在多人游戏中,失败的玩家所有人口都会弃掉,同时在哪些有人口的格放上一个野怪。
游戏胜利条件在行动阶段达成时就立刻胜利,而不需要执行后续的挑战行动。在单人游戏中,除了满足常规的胜利条件外,还需要根据版图上的 Wonder 数量拥有对应数量的殖民星球(但最多 4 个)。这些殖民星球需要在不同的区格,且不在母星系。玩家胜利后应给当前母星所在格标注上名字,这个格子会在后续游戏中刷多一个野怪。玩家可以创建一张文明卡,文明卡的增益效果和胜利条件以及所拥有的科技相关,不是完全自由选择。
不是每局胜利都会创造 Wonder 。需要玩家拥有至少 5 个同花色科技,才能以此花色创造 Wonder 。每个 Wonder 还需要和胜利模式组合。Wonder 以胜利玩家的母星位置标注在版图上,胜利模式和科技花色的组合以及 Wonder 地点不能在版图中重复。
这个游戏给我的启发很大。它有很多卡牌游戏和电子游戏的影子,但又非常独特。
不断制作卡牌的过程非常有趣,有十足的创造感。读规则书时我觉得我可能不会在玩的过程中给那些星球科技文明起名字,反正不影响游戏过程,留空也无所谓。但实际玩的时候,我的确会给三个半随机组合起来的完整科技卡起一个贴切的名称。因为创造一张完整的科技卡并不容易,我在玩的过程中就不断脑补这是一项怎样的科技,到可以起名的时候已经水到渠成了。
更别说胜利后创建文明卡。毕竟游戏的胜利来得颇为艰难。在失败多次后,脑海中已经呈现出一部太空歌剧,胜利的文明真的是踏着前人的遗产(那些创建出来的独有卡片)上成功。用心绘制一张文明卡真的是乐趣之一。我在 bgg 上看到有玩家精心绘制的带彩色头像的文明卡,心有戚戚。
游戏的平衡设计的非常好,有点难,但找到策略后系统也不是不可战胜的。关键是胜利策略会随着不断进行的游戏而动态变化:卡牌角标会因新卡的出现而改变概率分布,新的科技卡数量增加足以影响游戏策略,卡组里的星球科技会进化,星球在版图上的密度及分布也会变化…… 开局第一代策略和多个纪元的迭代后的策略可能完全不同,这让同一个战役(多局游戏的延展)的重玩价值很高。
用卡牌驱动随机性是一个亮点:以开始每种行动都是 6 张,均匀分布。但会因为星球卡打在桌面(从卡堆移除)而变化;更会因为创造新卡而变化。尤其是玩家可以通过 PLAN 主动创建特定花色卡片,这个创造过程也不是纯随机的,可以人为引导。负面事件的分布也会因此而收到影响。
用科技数量驱动负面事件数量是一个巧妙的设计。玩家获得胜利至少需要保有 6 个科技,即使在游戏后期纪元,也至少需要创造一个新科技,这会让游戏一定处于不断演变中。强力的桌面卡虽然一定程度的降低了游戏难度,但科技越多,每个回合潜在的负面事件也越多。以 3 科技开局的母星未必比单科技开局更容易,只是游戏策略不同而已。
每局游戏的科技必须创造出来(而不是打出过去游戏创造的科技牌)保证了游戏演变,也一定程度的平衡了游戏。即使过去的游戏创造出一张特别强力的科技,也不可以直接打在本局游戏的桌面。而只能做一次性消耗品使用。
一开始,负面事件的惩罚远高于单回合能获得的收益。在不太会玩的时候,往往三五回合就突然死亡了。看起来是脸黑导致的,但游戏建议玩家记录每局游戏的过程,一是形成一张波澜壮阔的银河历史,二是当玩家看到自己总是死于同一事件时有所反思,调整后续的游戏策略。
而战役的开局几乎都是白卡和低科技星球,一定程度的保护了新手玩家,平缓了游戏的学习曲线。边玩边做的模式让战役开局 setup 时间也不会太长,玩家也不会轻易放弃正常战役。
单局失败是很容易接受的,这是因为:单局时间很短,我单刷时最快 3 分钟一局,长局也很少超过 10 分钟。每局 setup 非常快。而游戏演化机制导致了玩家几乎不可能 undo 最近玩的一局,因为卡组已经永久的改变了。不光是新卡(因为只增加新卡的话,把新制造的卡片扔掉就可以 undeo ),还会在已有的卡牌上添加新的条目。
虽然我只玩了单人模式(并用新战役带朋友开了几局多人模式),但可以相像一个战役其实可以邀请其他玩家中途加入玩多人模式。多人模式采用协作还是对抗都可以,也可以混杂。协作和对抗会有不同的乐趣,同时都会进化战役本身。这在遗产类桌游中非常少见:大多数遗产类游戏都有一个预设的剧本和终局条件,大多推荐固定队伍来玩。但这个游戏没有终局胜利,只有不断创造的历史和不断演化的环境,玩家需要调整自己的策略玩下一局。
家附近有一个叫 35mm 的咖啡馆,以前经常去,某天心血来潮就想,会不会 35mm.coffee 正好是个可以注册的名呢?就上 Cloudflare 上看了下,果然可以,于是就注册了。即然注册了,就想着拿来做点什么,一开始把使用地限制在了 35MM 咖啡馆,也就是只有在那里才能使用,后来又将它改造成了只有晚上才能访问的网站,之后去掉了时间限制,改成可以随时访问的小 Twitter,还是不满意,总觉得少了一个明确的主题。
我是一个创作者,平时喜欢做点小东西,也会接触很多创作相关的内容,但总有种孤独感。回想起一开始入行的时候,学的是 ActionScript,那时经常逛一个论坛,氛围挺好的,自己写了一个什么小程序,或者学到了什么都会在那里分享,也从那里学到了不少,在交流过程中也结识了一些志同道合的朋友。
现在论坛已经成了过去式,创作者能逛的地方更多了,小红书、微博、推特、V2EX、知乎等等,但好像都没有特定主题的社区那种特有的氛围,也少了那种相对深入的、持续的、互相启发式的交流。Discord 其实蛮适合的,但它的封闭性和糟糕的桌面端是很大的问题。我开始思考,35mm.coffee 这个域名,是不是可以成为一个契机,重新找回那种失落的连接感,让它成为一个能让创作者们找到共鸣、答疑解惑、分享经验、甚至共同创作的地方?
35mm 代表着胶片时代经典的视角,一种记录真实、捕捉瞬间的工具。对于创作者而言,我们其实也正是在用自己的方式,记录着、捕捉着这个世界。
在这里,你可以:
最后,希望这个小站能重新找回那种失落的连接感,让创作之路不再孤单。
此题首先是不能暴力枚举的,因为 n 和 m 最大情况下是 10^9
,这个数据规模,暴力枚举肯定会超时。
然后我们可能想到贪心,但实际可落地的贪心的策略总是有特殊情况。
最后,假如我们可以检查一个答案是否可行,我们就可以用二分答案+判定的方法求解。
二分还有一个要求,就是答案是单调递增的。我们可以想像,随着兑换券的递增,如果限定 n 的值不变,那 m 的值肯定是递增的。所以此题符合单调递增的条件。
那么,对于一个可能的答案 k,我们怎么检查答案是否可行呢?
n-a
, m-b
来获得,也可以是 n-b
, m-a
来获得,我们让 d=a-b
n-a
变成了n-b
,相对来说,增加了 d,m 的值减少了 d所以:
c1=a*k
张课堂优秀券, c2=b*k
张作业优秀券c1 <=n, c2 <= m
那这个答案 k 显然就是可以的。c1 > n
,我们可以想到,把超额出来的兑换换成第二个兑换方法具体如何换呢?
c1-n
r=(c1-n)/d (向上取整)
即 r=(c1-n+d-1)/d
个d*r
,c2 的值增加了 d*r
最后需要注意,因为 a*k
的范围可能超过 int,所以需要把计算过程都用 long long 来保存。
此题考查了:
这还是非常综合的一道题。对于没想到二分的学生,也可以用贪心或者暴力枚举骗到不少分(估计 10-15 分),所以此题也有相当的区分度,各种能力的学生都可以拿到部分分数。
1 |
/* |
“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。之所以用这个隐喻,一方面是想让大家更容易理解、记忆和传播这个系统。
这个系统对我来说非常重要,它就相当于我人生的“第一性原理”一样。人类看起来是自己的主人,但人类对自身行为动机的理解很多时候并不清楚。
马斯洛把人类的需求按层次来分,在他的理论中提到的各种需求:性,安全,食物,社交,自我实现等等。但是其实,这些其实本质上,都是在为人类提供“多巴胺”。
当人类失去了“多巴胺”系统,很多时候就宁愿放弃生命:比如在战争中,很多人为了信仰而牺牲自己。这是因为他内心的目标大于活着的意义。
在实际生活中,虽然不至于放弃生命,但冒着生命危险做的事情,也不鲜见。比如消防队员救人、警察和歹徒搏斗、或者体育健儿在赛场上带伤为荣誉而战。
这些行为虽然有可能失去生命,但是换来的荣誉与成就是非常让人自豪的,可以为自己提供终身的多巴胺来源。
有人说,这个世界上只有两种生意:让人爽的生意和让人牛逼(学习、健身等)的生意。但我觉得,这都是多巴胺的生意,差别只是一个是提供短期多巴胺,一个是提供长期多巴胺。学习这种事情虽然短期很辛苦,但是收获的成就是可以提供长期的回报,从而提供长期的多巴胺。
看看全世界有多少人信教就明白了。大部分人都需要精神上为生命的存在赋予意义。意义感会驱使人们面对挑战和困难、提供情绪支撑、获得幸福感。
在中国,很少有人信教,但是我们每一个普通人也有自己对生命的追求,哪怕是更好一点的生活,或者一个遥不可及的理想,又或者是简单地照顾好家人和孩子。
人生的目标带动着每一个人在各种重大决策的十字路口上做选择。韩寒为了赛车辍学;赵心童为了台球远赴英国;崔永远为了自由表达离开了央视;而我身边,一个亲人为了更好的照顾孩子而放弃了工作上的晋升机会。
“多巴胺”系统就是为人生的意义提供基础能量的仓库,守护好多巴胺系统,人生之路就会走得更加从容。
我们随便看看身边,就会发现无论是学习、工作,还是退休安排和日常生活。“多巴胺”系统的构建都是非常不容易的。
拿学习来说,如果将孩子的“多巴胺”系统和学校排名、升学挂钩,那么很多孩子是无法构建学习的“多巴胺”系统的。因为每个班几十个孩子,必然有排在后面 50% 的孩子。这些孩子从排名上是无法获得正向激励的。
另外,整个学习是一个不断淘汰对手的游戏。中考会淘汰 50% 的学生分流到中专,高考又会分流 50% 的人到职高,大学又会分流 90% 的学生到非重点大学。研究生考试又会分流 2/3 的本科生,只剩下 1/3。
按上面的通过率,就算你是全中国前 1% 的学生,那大概也会止步于 985/211 的研究生入学考试。
所以,在学习上,你总会有一天会遇上身边的对手都比你强,你在这个小圈子里面排在后面,如果你和同学比的话,你能收获的只有负面的情绪,感觉自己像个废物。
后面我会提到如何构建学习的多巴胺系统。
也许你是一个优秀的员工,不断获得奖励和提拔,但是随着环境和年龄变化,工作中持续获得正反馈是困难的。原因如下:
第一个原因:正向激励的比例太低。只有前 20% 的员工才能获得超过其他人的回报,大部分人只能拿到普通的绩效和待遇。
第二个原因:很多工作的经验积累并不是线性的。在积累 3-5 年后,新增加的经验不足以带来相应比例产出提升,这就造成老员工工资过高,性价比不足。拿 iOS 开发来说,工作 10 年和工作 30 年的开发者的经验差异在大部分情况下表现得并不明显,这就可能造成某些工作 10 年以上的老员工薪资涨幅变慢。
第三个原因:人在 30 岁以后,体力和学习速度逐渐下降。我今年 41 岁,熬夜的能力明显变差。而我在 30 岁的时候,经常熬夜加班。工作中的一些内容如果需要的是堆时间才能完成,老员工的完成速度就不及年轻的员工。
第四个原因:岗位架构是金字塔形的。越往上需要的人越少,所以一个员工很容易最终就停在某一个岗位无法获得上升机会,背后的原因可能仅仅是因为上面已经有人了,不需要更多管理者。
退休是每个人必须面对的事情,如果不做好准备,“多巴胺”系统根本就不会自己产生。因为每个人退休后,日常生活的节奏就会有巨大变化。而人的时间是需要被填满的,否则就会因为意义感缺失而产生各种问题。
其它的部分还包括,生活、家庭、理财等等:
接下来,我就讲讲我对各种情况下构建“多巴胺”系统的心得。
对于学习,我们需要刻意设计“多巴胺时刻”。让原来可能没有的多巴胺变得有,让原来分泌得少的多巴胺,变得分泌多。具体来说,我们可以:
一、定期回顾,肯定自己的进步。我每年都会写年度总结,之前觉得每年没有什么变化,但是总结的时候,发现还是有挺多进步的,这样就让自己更有成就感。
二、设立奖励,自我颁奖。不管是小的学习还是大的学习,都可以设立奖励。我在做竞赛题的时候,之前做完我就继续做下一题。但后来我发现,如果我每次做对,都挥舞一下手臂小小庆祝一下,就会开心很多。所以,即便是很小的自我肯定,都可以让多巴胺给我们更多激励。
三、适当分享,获得亲朋鼓励。人是社会动物,自己的成就还是要适当分享出来。但是对自己友谊不深的朋友就没太有必要,有可能会造成人家妒忌,或者人家会认为你是一个喜欢炫耀的人,没必要。
四、构建无限游戏,不要设置终点和上限。学习无止境,如果我们可以一直设立目标,就可以无限玩下去。对于生命来说,能够无限玩的游戏不多,学习算是一个。
刚刚说过,随着环境和年龄变化,工作中持续获得正反馈是困难的。所以,对于工作,我们首先需要做的是降低预期。工作首先你是获得持续现金流的谋生手段;它如果能够给你持续的正向激励,当然很好,但是如果有一天,工作无法给你带来正反馈,那么你也可以就把它当作一份工作即可。
在工作上不要讲太多回报,公平。很多事情做了没有结果,但是公司付你钱了,所以你认真把事情做好,就很好,也很专业。
另外,在工作上,我们也需要尊重规律,做累进的事情。坚持在自己的专业领域积累经验,如果自己的年龄大了或者行业发展不好,也要接受工资不再上涨这些现实。
在工作上,我们还可以尝试杠铃策略,即:同时拥有两个不太相关的专业技能。通过在业余时间利用自己的爱好或者特长来发展副业,如果万一出现什么变动,自己的副业就可以成为主业,保证自己不至于失业。
退休是人一辈子重要的规划之一,也是人生活法的重大转换。
对于退休,最重要的事情就是让提前规划好兴趣,让兴趣填满自己的时间。否则,人生一下子多了那么多时间,很容易觉得无聊。
这个兴趣最好是无限挑战游戏。这样可以几十年也做不完。
这个兴趣也最好可以锻炼到身体(例如:广场舞、摄影、骑行之类)。
最后,退休还有一个很重要的事情:要管好自己的钱,不冒大的风险,不折腾高风险的投资。因为挣太多钱自己也不一定能花完,但是如果亏很多就会影响自己的退休生活。
日常生活中,有这些技巧可以带来更多的多巴胺:
一、主动给生活带来变化
我自己的经验是,主动做一些以前没做过的事情,会给生活带来新鲜感。比如:
二、自立
不要太依赖别人,或者太依赖于某个工作,或者将自己放到一个困境,或者太陷入一个目标。这不是说我们应该不努力。对于生活,我们应该全情投入,把过程做好;但是对于结果,我们应该顺其自然。
三、终身学习
学习是少有的,可以持续给人带来获得感的事情。而且这个事情是没有终点的,属于一种“无限游戏”,这就让我们永远不会觉得无聊。
我最近因为兴趣又开始学习编程,遇到一个算法没看懂,我就慢慢想,可能想个一周,甚至两周,我感觉这才是一个学习的状态,就是慢慢的,不紧不慢的,学完一个再学下一个。
相对来说,学校的学习更像是一个工业化的人才产出器,每个人需要像机器一样在指定的时间学习完指定的内容,但是每个人的学习能力是不一样的,其实对每个人来说,匹配自己的学习速度才是最佳的学习方案。
四、关注过程,弱化结果
人生是一场体验,并非要留下什么,也留不下什么。
如果我们想想 100 年后谁能记得我们,我们会发现结论是:没有人。即使是自己的亲人,过了三代你可能也不会记得。大家可以想想,你知道你的爷爷的爷爷叫什么名字,长什么样,做过什么成绩吗?就算你记得,你的孩子以后会记得吗?
所以,如果人生到最后不会有任何人记得我们,那么我们人生的意义是什么?我认为核心的意义就是人生本身。就像《活着》中写道:活着就是最大的意义。
对于人生这种重过程,无结果的“游戏”,我们活在当下,关注过程,把自己的人生过好,就是一个非常棒的事情了。别的更多的结果,我们做不到,也没有什么意义。
对于家庭,最简单的获得多巴胺的方式是:低预期。比如:
对于家人,不要指望家人一定要为自己付出。家人能够不让你付出,就是超预期。有这样的心态,你每天都是超预期。
对于孩子也一样,低预期,不鸡娃。
我认为有三种朋友,可以给我们提供持续的多巴胺。
那哪些是消耗你多巴胺的朋友呢?
我有些时候,有点讨好型人格,就是不喜欢一个人,也不愿意和人家起冲突,很多时候碍于面子还是淡淡地交往。后来我发现这样不对,这完全是一种对多巴胺系统的伤害,想到这些我就主动断开了一些不喜欢的朋友的来往。其实有一些人是很优秀的,但是多巴胺系统为先的决策,让我还是会坚决断开联系。
小孩子如果反复盯着糖果看,最后就会忍不住吃掉糖果。如果有人伤害了你,你反复回忆这个伤害的过程,你就会受到更多的内心部分的伤害。
著名作家蔡澜最近去世了,别人问他,他的爱人离他而去了,他是如何克服下来的。蔡澜说:你如果老去想这件事情,你就会发疯,所以我尽量让自己不去想这件事情。
芒格和巴菲特的公司之前特别看好一个接班人,后来这个接班人做了一些违背公司原则的事情,在收购一家公司前,自己私下提前买了这家公司的股票,自己获利了几百万美元。事情暴露之后,这个接班人辞职了。别人问芒格怎么看这个事情。
面对欺骗与背叛,芒格说:永远不要责备自己,永远不要有受害者心态。当你产生这种心态的时候,只会让你自己难受,不会带来任何其它正面的影响,因此你不应该花时间去感受它,哪怕是一秒钟。所以,更应该的心态是应对这种情况,为未来的不确定性做好准备。
芒格最后总结道:“I am not a victim. I am a survivor.”
所以,站在建立“多巴胺”系统的角度,任何只有负面效果的情绪都是不值得去强化和感受的。如果你忍不住,你可以尽量不去想它。更好的办法是像芒格那样,有一个更加强大的幸存者视角来看待所有的坏运气、灾难、欺骗与背叛。让这些负面情绪不影响自己的多巴胺系统。
我后来发现,其他人讲的一些行事原则,在表达角度上虽然不一样,其实也是一样的道理。比如我们讲的“不内耗”原则。
内耗就是一种持续消耗“多巴胺”的心理行为。如果以构建“多巴胺”系统作为人生准则的话,我们会发现内耗没有任何效果。当我们面对不如意的时候,要么改变,要么适应,要么淡化,而内耗是一种既不改变,又不适应,又反复强化负反馈的行为。百害而无一利。
自恰的底层含义是:所有事情能够自圆其说,不矛盾,不冲突,自然也就不内耗了,不消耗多巴胺。
所以,人需要活得“自恰”,只有自恰才能睡好觉,持续获得多巴胺。
“多巴胺”系统有主观的部分,也有客观的部分。
一、主观部分
“多巴胺”系统对于个人内心是一种主观行为和感受,而不是一种客观描述和标准。所以,对于芒格来说,一个重要朋友的背叛不是对“多巴胺”系统的冲击;但换一个人,可能觉得天塌了,一辈子再难信任他人。
因此,我们更应该调整的是自我的行事方式和思考问题的角度,而不是改变其他人。我们可以远离那些影响我们“多巴胺”系统的人和事,但是当坏运气到来的时候,我们只能接受。
二、客观部分
当然,“多巴胺”系统在指导我们行为的时候,是让我们客观上在做具体的行为选择。通过行为选择让我们尽可能构建有利于我们产生多巴胺的外界环境。比如我刚刚提到的:提前规划退休生活、选择终身学习、多搞庆祝活动等。这些有利的环境不但不会消耗我们主观意志来维护多巴胺,还会给我们提供愉悦,贡献多巴胺。
“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。我们通过:
利用“多巴胺”系统,让自己的人生少一点内耗,少一点纠结,多一点平静,多一点快乐。
愿每个读者都能过好当下的每一天,谢谢!
在移动端 UI 设计中,弹窗(Dialog) 是承载「打断式沟通」(interrupt communication) 的核心控件:它能在适当时机抓住用户注意力,提示风险、请求确认或引导后续操作。下面我沿着 「系统 → 半自定义 → 全自定义」 的脉络,简单说明在Flutter里面写出可扩展的弹窗。
本文完整Demo代码: github.com/wutao23yzd/… 中的Demo6
效果如下所示:
AlertDialog
——最常见的模态对话框Future<void> _showDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
title: const Text('弹窗标题'),
content: const Text('这是一个弹窗内容。'),
actions: [
TextButton(child: const Text('取消'), onPressed: () => Navigator.pop(context)),
TextButton(child: const Text('确定'), onPressed: () => Navigator.pop(context)),
],
),
);
}
showDialog
是 Flutter 内置的异步 API;Future
在对话框关闭后完成。barrierDismissible: true
允许点击遮罩关闭,用户体验更友好。AlertDialog
自带标题、正文、按钮插槽,适合提示 + 二次确认的场景。SimpleDialog
——最简单的选项列表Future<void> _changeLanguage() async {
int? result = await showDialog<int>(
context: context,
builder: (context) => SimpleDialog(
title: const Text('选择语言'),
children: [
SimpleDialogOption(child: const Text('中文'), onPressed: () => Navigator.pop(context, 1)),
SimpleDialogOption(child: const Text('English'), onPressed: () => Navigator.pop(context, 2)),
],
),
);
}
SimpleDialog
省去了布局细节,只应对轻量级多选一;返回值 result
让你能在调用处直接拿到用户选择。
Future<void> _showBottomSheet() async {
return showModalBottomSheet<void>(
context: context,
builder: (context) => Wrap(
children: [
ListTile(title: const Center(child: Text('选项 1')), onTap: () => Navigator.pop(context)),
ListTile(title: const Center(child: Text('选项 2')), onTap: () => Navigator.pop(context)),
const Divider(),
ListTile(
title: const Center(child: Text('取消', style: TextStyle(color: Colors.red))),
onTap: () => Navigator.pop(context),
),
],
),
);
}
showModalBottomSheet
默认带遮罩、支持手势下滑关闭;用 Wrap
自适应高度。系统组件虽方便,但在品牌一致性、复杂交互、动画细节上往往力不从心。在Demo的自定义方案中,拆成 数据层 + 动效层 + API 层。
ModalOption
class ModalOption {
final String? name; // 选项文案
final Widget? icon; // 自定义图标
final IconData? iconData; // 或者用系统 Icon
final Widget? child; // 复杂场景直接塞子组件
final VoidCallback? _onTap; // 点击回调
final bool distractive; // 危险操作标识(高亮红色)
...
}
copyWith
保留 immutable 风格,后续修改也安全。distractiveColor
把「危险高亮」逻辑封装内部,外部不用再判红色。Tappable
class Tappable extends StatefulWidget {
const Tappable.faded({ required this.child, this.onTap, FadeStrength fadeStrength = FadeStrength.md });
...
}
AnimationController
+ FadeTransition
实现轻量「按下变暗 / 松手复原」的Material 触感。normal
) 与半透明 (faded
) 两种模式,可在不同场景复用。BuildContext
扩展方法 | 用途 | 关键点 |
---|---|---|
showAdaptiveDialog |
包一层 AlertDialog.adaptive ,自动适配 iOS / Android 风格 |
同时暴露 titleTextStyle ,便于定制 |
showBottomModal |
对 showModalBottomSheet 包装 |
内置圆角 & DragHandle,可自由开关 |
showListOptionsModal |
带滚动的选项列表 | 组合 ModalOption + Tappable ;点击后 Navigator.pop 将选项回传 |
showImagePreview |
圆形图片预览弹窗 |
AspectRatio + DecorationImage 让图片保持清晰 |
把重复的 shape / safeArea / isDismissible 等参数收拢在扩展方法中——调用端只关注内容和交互,让业务代码最小化。
OutlinedButton(
child: const Text('自定义选项弹窗'),
onPressed: () {
context.showListOptionsModal(
title: '新建',
options: createMediaModalOptions(...),
).then((option) => option?.onTap(context));
},
);
context.showImagePreview('https://picsum.photos/id/237/300/200');
Dialog
背景透明 (backgroundColor: Color(0x00000000)
),中间圆形头像带描边。context.showListOptionsModal(
options: [
ModalOption(child: const LocaleModalOption()),
ModalOption(child: const ThemeSelectorModalOption()),
ModalOption(child: const LogoutModalOption()),
],
);
每个 ModalOption
里直接塞自定义子组件,如 DropdownButton
语言/主题选择,实现**「边选边生效」**的即时交互体验。
Tappable
):让自定义弹窗拥有系统级触感,提升专业度。distractiveColor
这样封装「红色」逻辑,可避免遗漏、风格不一致。通过这些实践,基本能在 Flutter 中构建出自定义且易于迭代的弹窗体系,真正让「对话」成为强化用户体验的利器。
写在最后:本文代码有参考www.youtube.com/watch?v=xr5…
1、由于系统限制,iOS无法禁止用户的截屏行为;只有在发生截屏时触发了一个截屏通知-- UIApplication.userDidTakeScreenshotNotification
;
2、但是这个通知只起到告知作用,收到这个通知时,截屏已经发生了,截屏的内容会以图片的方式存到相册;
3、从系统特性层面来看,似乎无法限制iOS防截屏;
4、即然系统无法限制截屏,那我们就想办法修改截屏的内容呗!!!让截屏截了个寂寞😂
基于 UITextField
的安全文本输入特性(isSecureTextEntry
)以及 私有视图层级的利用,从而在截屏或录屏时隐藏敏感内容。
现成的第三方库:github.com/RyukieSama/…
pod 'RyukieSwifty/ScreenShield'
ScreenShield的用法很简单,我就不介绍了,有兴趣的自己玩儿。
1、首先在UIViewController的loadView
方法中设置 self.view
为ScreenShieldView
;
2、然后布局在self.view
上的子视图都会加到安全图层上,整个页面就具有了防截屏功能;
3、当用户截屏时,截屏出来的将是一个空白页面,这样就起到了防截屏,防录屏的作用;
真正使用过程中会遇到如下问题:
问题一:self.view.subviews数组存放的子视图不是真正添加到self.view上的子视图?,就是说无法通过self.view.subviews获取子视图数量了
查看ScreenShieldView
的原码可以发现,虽然我们在loadView中将self.view
设置成了ScreenShieldView
,并且是通过self.view添加的子视图,但其实所有子视图都添加到了ScreenShieldView
的safeZone
上了。
分析问题
想重写subviews方法?可以试一试!
public override var subviews: [UIView]{
guard let safe = safeZone else {
return super.subviews
}
return safe.subviews
}
运行发现所有添加到self.view上的子视图都不显示了
subviews
不止开发者会调用,UIView内容系统也会调用,重写了subviews后系统调用的将是重写后的函数,并且返回的将是safe.subviews
;safeZone
上的,但是设置的约束都是与self.view
的。重写subviews函数后,约束设置会失效;约束布局失效
,所有通过约束添加到self.view上的子视图都将无法正常显示。但是不影响frame布局
。解决方案
实现一个新的获取子视图的函数来获取真正的子视图
public var safeSubviews: [UIView]{
guard let safe = safeZone else {
return super.subviews
}
return safe.subviews
}
问题二:ScreenShieldView
无法被继承,因为ScreenShieldView只能通过create
函数创建才有防截屏图层
分析问题
如果我们想在某个自定义的View中也加上防截屏图层,但是呢又不想改变View的初始化方法,但是通过将View继承自ScreenShieldView又行不通,因为ScreenShieldView只能通过create
函数创建。
解决方案
既然不能继承,那可以加个中间层,我们可以创建一个BaseView
,将ScreenShieldView添加到BaseView上,然后像ScreenShieldView的实现方式一样,将所有添加子视图的方法全部重写。
//
// BaseScreenShieldView.swift
// ScreenShieldDemo
//
// Created by 熊进辉 on 2025/7/12
// Copyright © 2025/7/12 datacloak. All rights reserved.
//
import UIKit
import SnapKit
class BaseScreenShieldView: UIView {
private var contentView:ScreenShieldView? = nil
override init(frame: CGRect) {
super.init(frame: frame)
self.setupContent()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupContent(){
self.contentView = ScreenShieldView.create(frame: CGRectZero)
self.addSubview(self.contentView!)
self.contentView?.snp.makeConstraints { make in
make.top.equalTo(0)
make.leading.equalTo(0)
make.bottom.equalTo(0)
make.trailing.equalTo(0)
}
}
override func addSubview(_ view: UIView) {
if (contentView != nil) {
contentView!.addSubview(view)
} else {
super.addSubview(view)
}
}
override func insertSubview(_ view: UIView, at index: Int) {
if (contentView != nil) {
contentView!.insertSubview(view, at: index)
} else {
super.insertSubview(view, at: index)
}
}
override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
if (contentView != nil) {
contentView!.insertSubview(view, aboveSubview: siblingSubview)
} else {
super.insertSubview(view, aboveSubview: siblingSubview)
}
}
override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
if (contentView != nil) {
contentView!.insertSubview(view, belowSubview: siblingSubview)
} else {
super.insertSubview(view, belowSubview: siblingSubview)
}
}
override func exchangeSubview(at index1: Int, withSubviewAt index2: Int) {
if (contentView != nil) {
contentView!.exchangeSubview(at: index1, withSubviewAt: index2)
} else {
super.exchangeSubview(at: index1, withSubviewAt: index2)
}
}
override func bringSubviewToFront(_ view: UIView) {
if (contentView != nil) {
contentView!.bringSubviewToFront(view)
} else {
super.bringSubviewToFront(view)
}
}
override func sendSubviewToBack(_ view: UIView) {
if (contentView != nil) {
contentView!.sendSubviewToBack(view)
} else {
super.sendSubviewToBack(view)
}
}
}
问题三:如何定制化截屏样式
分析问题 先来看添加ScreenShieldView后的图层
1、可以看到ViewController上的图层self.view就是ScreenShieldView图层;
2、self.view的子视图是添加在_UITextLayoutCanvasView
上的,即safeZone
;
再来看看ScreenShieldView的safeZone是添加在哪里的
分析代码发现,safeZone是添加到ScreenShieldView的;
结合图层和代码分析,要显示的内容需要添加到safeZone
上,而要定制化的截屏图层需要放在ScreenShieldView上
,并且在safeZone图层下方
。
解决方案 在safeZone下方放一个protectedView
@objc public static func create(frame: CGRect = .zero, protectedView: UIView?) -> ScreenShieldView {
return ScreenShieldView(frame: frame,protectedView: protectedView)
}
private init(frame: CGRect, protectedView: UIView?) {
super.init(frame: frame)
safeZone = makeSecureView() ?? UIView()
self.protectedView = protectedView
if let sf = safeZone {
if self.protectedView != nil {
self.protectedView?.removeFromSuperview()
super.addSubview(self.protectedView!)
self.protectedView!.snp.makeConstraints { make in
make.top.equalTo(0)
make.bottom.equalTo(0)
make.left.equalTo(0)
make.right.equalTo(0)
}
}
addSubview(sf)
let layoutDefaultLowPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue-1)
let layoutDefaultHighPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue-1)
sf.translatesAutoresizingMaskIntoConstraints = false
sf.setContentHuggingPriority(layoutDefaultLowPriority, for: .vertical)
sf.setContentHuggingPriority(layoutDefaultLowPriority, for: .horizontal)
sf.setContentCompressionResistancePriority(layoutDefaultHighPriority, for: .vertical)
sf.setContentCompressionResistancePriority(layoutDefaultHighPriority, for: .horizontal)
let top = NSLayoutConstraint.init(item: sf, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)
let bottom = NSLayoutConstraint.init(item: sf, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
let leading = NSLayoutConstraint.init(item: sf, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0)
let trailing = NSLayoutConstraint.init(item: sf, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)
self.addConstraints([top, bottom, leading, trailing])
}
}
通过传递进来的protectedView
添加到ScreenShieldView上,将protectedView的背景颜色设置为红色,将self.view的背景颜色设置为黄色;然后显示时,页面显示红色,再截屏,截屏出来的图片背景也是红色的。说明这样做是可以定制截屏图层
的。
但是同样引入了一个新的问题
:设置的self.view的黄色不生效
了,也就是说虽然定制了截屏页面样式,但是这个样式成为了所有的页面背景。
这不是我们想要的,我们想要的是只有截屏时,这个截屏图层才显示在截屏图片上,正常情况下不要显示出来。
解决截屏图层异常显示的问题
我们在safeZone的子视图最下面的图层
上,再放置一个图层,用于显示self.view的背景颜色,然后重写背景颜色的setter
函数
//修改背景颜色
public override var backgroundColor: UIColor? {
get {
super.backgroundColor
}
set {
super.backgroundColor = newValue
self.portiereView?.backgroundColor = newValue
}
}
private init(frame: CGRect, protectedView: UIView?) {
上面的代码不变...
if self.protectedView != nil {
let portiereView = UIView()
portiereView.backgroundColor = .white
self.addSubview(portiereView)
portiereView.snp.makeConstraints { make in
make.top.equalTo(0)
make.bottom.equalTo(0)
make.left.equalTo(0)
make.right.equalTo(0)
}
self.portiereView = portiereView
}
}
上面的操作虽然解决了self.view背景颜色失效的问题,但是改变了safeZone的子视图数量,所以要修改以下函数:
public override func insertSubview(_ view: UIView, at index: Int) {
guard
let safe = safeZone,
view != safeZone
else {
super.insertSubview(view, at: index)
return
}
if self.protectedView != nil ,index == 0 {
safe.insertSubview(view, at: 1)
}else{
safe.insertSubview(view, at: index)
}
}
public override func exchangeSubview(at index1: Int, withSubviewAt index2: Int) {
guard
let safe = safeZone
else {
super.exchangeSubview(at: index1, withSubviewAt: index2)
return
}
safe.exchangeSubview(at: index1, withSubviewAt: index2)
}
问题四:ScreenShieldView如何应用到swiftUI中
问题分析 根据swiftUI的特性,图层都是一层一层叠上去的,所以我们应该将ScreenShieldView放在body的最下方
解决方案
使用UIViewRepresentable
将ScreenShieldView进行封装,使其可以在swiftUI上使用
import SwiftUI
struct ScreenShieldSwiftUIView<Content: View>: UIViewRepresentable{
let content: Content
let antiScreenshot:Bool
init(antiScreenshot:Bool, @ViewBuilder content: () -> Content ) {
self.antiScreenshot = antiScreenshot
self.content = content()
}
// MARK: - Coordinator 用于缓存 HostingController
class Coordinator {
var hostingController: UIHostingController<Content>?
init(_ hostingController: UIHostingController<Content>?) {
self.hostingController = hostingController
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(nil)
}
func makeUIView(context: Context) -> UIView {
var shieldView:UIView
if self.antiScreenshot == true{
let protectedView = UIView();
shieldView = ScreenShieldView.create(frame: .zero,protectedView: protectedView)
}else{
shieldView = UIView()
}
let hostingController = UIHostingController(rootView: content)
context.coordinator.hostingController = hostingController
let hostedView = hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = false //是 Auto Layout 中的一个属性,用于控制视图的自动布局行为。当你手动使用 Auto Layout(比如添加约束)时,这个属性的设置至关重要。
shieldView.addSubview(hostedView)
NSLayoutConstraint.activate([
hostedView.topAnchor.constraint(equalTo: shieldView.topAnchor),
hostedView.bottomAnchor.constraint(equalTo: shieldView.bottomAnchor),
hostedView.leadingAnchor.constraint(equalTo: shieldView.leadingAnchor),
hostedView.trailingAnchor.constraint(equalTo: shieldView.trailingAnchor)
])
return shieldView
}
func updateUIView(_ uiView: UIView, context: Context) {
// 关键:在状态变化时更新 rootView 内容
context.coordinator.hostingController?.rootView = content
}
}
iOS无法禁止用户的截屏,但是可以通过一定的手段将修改截屏的内容。但是整个实现的过程也是在解决问题、引入新问题、再解决新问题的过程中不断的探索。在iOS系统特性的基础上,按业务需求进行取舍的过程。
错误处理是Swift编程语言中的重要特性,它提供了一种优雅的方式来处理运行时可能出现的错误情况。Swift的错误处理机制基于抛出、捕获、传播和操作可恢复错误。
Swift中的错误处理涉及四个关键字:
throw
:抛出错误throws
:标记可能抛出错误的函数try
:调用可能抛出错误的函数catch
:捕获并处理错误// 1. 语法错误(编译时报错)
// let x = 10 + // 语法错误,会在编译时发现
// 2. 逻辑错误(程序运行但结果不正确)
func add(a: Int, b: Int) -> Int {
return a * b // 逻辑错误:应该是加法但写成了乘法
}
// 3. 运行时错误(可能导致程序崩溃)
// let array = [1, 2, 3]
// let item = array[10] // 运行时错误:数组越界
// 使用枚举定义错误类型
enum ValidationError: Error {
case emptyString
case tooShort(minimum: Int)
case tooLong(maximum: Int)
case invalidFormat
}
enum FileError: Error {
case notFound
case permissionDenied
case corrupted
case networkError(String)
}
enum MathError: Error {
case divisionByZero
case negativeSquareRoot
case overflow
case underflow
}
struct CustomError: Error {
let code: Int
let message: String
let underlyingError: Error?
init(code: Int, message: String, underlyingError: Error? = nil) {
self.code = code
self.message = message
self.underlyingError = underlyingError
}
}
func divide(_ numerator: Int, by denominator: Int) throws -> Double {
if denominator == 0 {
throw MathError.divisionByZero
}
return Double(numerator) / Double(denominator)
}
func validatePassword(_ password: String) throws -> Bool {
if password.isEmpty {
throw ValidationError.emptyString
}
if password.count < 6 {
throw ValidationError.tooShort(minimum: 6)
}
if password.count > 20 {
throw ValidationError.tooLong(maximum: 20)
}
return true
}
func processFile(at path: String) throws -> String {
// 检查文件是否存在
guard FileManager.default.fileExists(atPath: path) else {
throw FileError.notFound
}
// 检查文件权限
guard FileManager.default.isReadableFile(atPath: path) else {
throw FileError.permissionDenied
}
// 读取文件内容
do {
let content = try String(contentsOfFile: path)
return content
} catch {
throw FileError.corrupted
}
}
func testDivision() {
do {
let result = try divide(10, by: 2)
print("结果: \(result)")
} catch MathError.divisionByZero {
print("错误: 除数不能为零")
} catch {
print("其他错误: \(error)")
}
}
func handleValidation() {
do {
try validatePassword("123")
print("密码验证通过")
} catch ValidationError.emptyString {
print("密码不能为空")
} catch ValidationError.tooShort(let minimum) {
print("密码太短,至少需要 \(minimum) 个字符")
} catch ValidationError.tooLong(let maximum) {
print("密码太长,最多允许 \(maximum) 个字符")
} catch ValidationError.invalidFormat {
print("密码格式不正确")
} catch {
print("未知错误: \(error)")
}
}
func processMultipleOperations() {
do {
let result1 = try divide(10, by: 2)
let result2 = try divide(20, by: 4)
let finalResult = result1 + result2
print("最终结果: \(finalResult)")
} catch MathError.divisionByZero {
print("除法错误:除数为零")
} catch {
print("操作失败: \(error)")
}
}
func handleFileOperation() {
do {
let content = try processFile(at: "/path/to/file.txt")
print("文件内容: \(content)")
} catch let error as FileError {
switch error {
case .notFound:
print("文件不存在")
case .permissionDenied:
print("没有文件读取权限")
case .corrupted:
print("文件已损坏")
case .networkError(let message):
print("网络错误: \(message)")
}
} catch {
print("其他错误: \(error)")
}
}
func safeOperation() {
// try? 将错误转换为可选值
let result1 = try? divide(10, by: 2) // Optional(5.0)
let result2 = try? divide(10, by: 0) // nil
print("结果1: \(result1 ?? 0)")
print("结果2: \(result2 ?? 0)")
}
// try? 等价于以下代码
func equivalentOperation() {
var result: Double?
do {
result = try divide(10, by: 2)
} catch {
result = nil
}
print("结果: \(result ?? 0)")
}
func forcedOperation() {
// try! 假设操作不会失败,如果失败则程序崩溃
let result = try! divide(10, by: 2) // 5.0
print("结果: \(result)")
// 危险的用法 - 如果失败会导致程序崩溃
// let badResult = try! divide(10, by: 0) // 运行时崩溃
}
func readFile(fileName: String) throws -> String {
let file = FileHandle(forReadingAtPath: fileName)
defer {
file?.closeFile()
print("文件已关闭")
}
guard let file = file else {
throw FileError.notFound
}
let data = file.readDataToEndOfFile()
return String(data: data, encoding: .utf8) ?? ""
}
func complexOperation() throws {
print("开始复杂操作")
defer {
print("清理操作1")
}
defer {
print("清理操作2")
}
defer {
print("清理操作3")
}
// 模拟一些操作
throw ValidationError.emptyString
}
// 输出顺序:
// 开始复杂操作
// 清理操作3
// 清理操作2
// 清理操作1
func lowLevelOperation() throws -> String {
throw ValidationError.emptyString
}
func midLevelOperation() throws -> String {
return try lowLevelOperation()
}
func highLevelOperation() throws -> String {
return try midLevelOperation()
}
func handlePropagation() {
do {
let result = try highLevelOperation()
print("操作成功: \(result)")
} catch {
print("操作失败: \(error)")
}
}
func convertError() throws -> String {
do {
return try lowLevelOperation()
} catch ValidationError.emptyString {
throw CustomError(code: 100, message: "输入验证失败")
}
}
enum NetworkError: Error {
case noConnection
case serverError(Int)
case invalidResponse
case decodingError
}
class NetworkManager {
func fetchData(from url: URL) throws -> Data {
// 模拟网络请求
let isConnected = true
let statusCode = 200
guard isConnected else {
throw NetworkError.noConnection
}
guard statusCode == 200 else {
throw NetworkError.serverError(statusCode)
}
return Data()
}
func fetchUserData(userId: Int) throws -> User {
let url = URL(string: "https://api.example.com/users/\(userId)")!
let data = try fetchData(from: url)
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
throw NetworkError.decodingError
}
}
}
struct User: Codable {
let id: Int
let name: String
}
class FormValidator {
func validateEmail(_ email: String) throws -> Bool {
if email.isEmpty {
throw ValidationError.emptyString
}
if !email.contains("@") {
throw ValidationError.invalidFormat
}
return true
}
func validateForm(email: String, password: String) -> [Error] {
var errors: [Error] = []
do {
try validateEmail(email)
} catch {
errors.append(error)
}
do {
try validatePassword(password)
} catch {
errors.append(error)
}
return errors
}
}
// 好的做法:使用枚举定义相关错误
enum DatabaseError: Error {
case connectionFailed
case queryFailed(String)
case dataCorrupted
case timeout
}
// 避免:使用通用错误类型
// struct GenericError: Error { let message: String }
extension ValidationError: LocalizedError {
var errorDescription: String? {
switch self {
case .emptyString:
return "输入不能为空"
case .tooShort(let minimum):
return "输入长度不能少于\(minimum)个字符"
case .tooLong(let maximum):
return "输入长度不能超过\(maximum)个字符"
case .invalidFormat:
return "输入格式不正确"
}
}
}
// 策略1:立即处理错误
func immediateHandling() {
do {
let result = try divide(10, by: 0)
print("结果: \(result)")
} catch {
print("发生错误,使用默认值")
let defaultResult = 0.0
print("结果: \(defaultResult)")
}
}
// 策略2:传播错误
func propagateError() throws {
let result = try divide(10, by: 0)
print("结果: \(result)")
}
// 策略3:转换为可选值
func optionalHandling() {
let result = try? divide(10, by: 0)
print("结果: \(result ?? 0)")
}
Error
协议,通常使用枚举throws
标记可能抛出错误的函数throw
关键字抛出错误try
、do-catch
语句处理错误defer
语句确保资源被正确释放Swift的错误处理机制提供了一种类型安全且表达力强的方式来处理运行时错误。通过合理使用错误处理,我们可以:
掌握错误处理是编写高质量Swift代码的重要技能。
本文档基于Swift 5.0+版本,涵盖了错误处理的核心概念和最佳实践。
实例方法(Instance Method)是属于特定类、结构体或枚举的实例的方法。
struct Counter {
var count = 0
// 实例方法
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
var counter = Counter()
counter.increment()
counter.increment(by: 5)
counter.reset()
self
属性是每个实例隐式拥有的属性,完全等同于该实例本身。
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x // 区分参数x和属性x
}
}
let point = Point(x: 4.0, y: 5.0)
print(point.isToTheRightOf(x: 1.0)) // true
通常情况下,不需要显式地写出self
,Swift会自动推断。但在以下情况下需要使用:
struct Calculator {
var result: Double = 0
func add(_ value: Double) -> Calculator {
result += value
return self // 返回自身,支持链式调用
}
func multiply(_ value: Double) -> Calculator {
result *= value
return self
}
}
let calculator = Calculator()
let result = calculator.add(5).multiply(2).result // 10
值类型(结构体、枚举)的实例方法默认不能修改实例的属性。如果需要修改,必须使用mutating
关键字。
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var point = Point(x: 1.0, y: 1.0)
point.moveBy(x: 2.0, y: 3.0)
print(point) // Point(x: 3.0, y: 4.0)
mutating
关键字mutating
关键字mutating
方法可以为self
赋予一个全新的实例struct Point {
var x = 0.0, y = 0.0
mutating func moveToOrigin() {
self = Point(x: 0.0, y: 0.0)
}
}
enum TriStateSwitch {
case off, low, high
mutating func next() {
switch self {
case .off:
self = .low
case .low:
self = .high
case .high:
self = .off
}
}
}
var lightSwitch = TriStateSwitch.low
lightSwitch.next() // .high
lightSwitch.next() // .off
@discardableResult
特性用于标记那些有返回值但调用者可以忽略返回值的方法。
struct Stack<T> {
var items: [T] = []
mutating func push(_ item: T) {
items.append(item)
}
@discardableResult
mutating func pop() -> T? {
return items.popLast()
}
}
var stack = Stack<Int>()
stack.push(1)
stack.push(2)
// 不使用@discardableResult会产生警告
let popped = stack.pop() // 使用返回值
stack.pop() // 忽略返回值,不会产生警告
class Logger {
@discardableResult
func log(_ message: String) -> String {
let timestamp = Date().description
let logEntry = "[\(timestamp)] \(message)"
print(logEntry)
return logEntry
}
}
let logger = Logger()
logger.log("Error occurred") // 忽略返回值
let entry = logger.log("Info log") // 使用返回值
类型方法(Type Method)是属于类型本身的方法,而不是属于类型的某个实例。
struct MathUtils {
// 类型方法
static func abs(_ number: Int) -> Int {
return number < 0 ? -number : number
}
static func max(_ a: Int, _ b: Int) -> Int {
return a > b ? a : b
}
}
// 调用类型方法
let result1 = MathUtils.abs(-10) // 10
let result2 = MathUtils.max(5, 8) // 8
static
:不能被子类重写class
:可以被子类重写(仅限于类)class Vehicle {
static func staticMethod() {
print("Vehicle static method")
}
class func classMethod() {
print("Vehicle class method")
}
}
class Car: Vehicle {
// 不能重写static方法
// override static func staticMethod() { } // 编译错误
// 可以重写class方法
override class func classMethod() {
print("Car class method")
}
}
Vehicle.staticMethod() // Vehicle static method
Car.staticMethod() // Vehicle static method
Vehicle.classMethod() // Vehicle class method
Car.classMethod() // Car class method
struct Temperature {
var celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
// 类型方法:工厂方法
static func fromFahrenheit(_ fahrenheit: Double) -> Temperature {
return Temperature(celsius: (fahrenheit - 32) * 5/9)
}
static func fromKelvin(_ kelvin: Double) -> Temperature {
return Temperature(celsius: kelvin - 273.15)
}
}
let temp1 = Temperature(celsius: 25.0)
let temp2 = Temperature.fromFahrenheit(77.0)
let temp3 = Temperature.fromKelvin(298.15)
在类型方法中,self
指向类型本身:
struct Counter {
static var count = 0
static func increment() {
self.count += 1 // self指向Counter类型
}
static func reset() {
count = 0 // 可以省略self
}
}
Counter.increment()
print(Counter.count) // 1
Counter.reset()
print(Counter.count) // 0
方法类型 | 调用方式 | 访问范围 | 修改实例 | 适用类型 |
---|---|---|---|---|
实例方法 | 实例.方法() | 实例属性和方法 | 需要mutating | 类、结构体、枚举 |
类型方法 | 类型.方法() | 类型属性和方法 | 不涉及实例 | 类、结构体、枚举 |
mutating
:值类型的实例方法修改属性时必须使用@discardableResult
:标记可忽略返回值的方法static
:定义不可重写的类型方法class
:定义可重写的类型方法(仅限类)self
:当前实例(实例方法)或类型(类型方法)的引用在Swift中,可以通过func
定义一个函数,也可以通过闭包表达式定义一个函数。
下面准确来说应该说是一个闭包表达式,而还有一个闭包是函数套函数,内部函数使用外部函数的局部变量,内部函数和变量才构成真正的闭包。
{
(参数列表) -> 返回值类型 in
函数体代码
}
普通函数定义:
func sum(_ v1: Int, _ v2: Int) -> Int {
v1 + v2
}
闭包表达式定义:
var fn = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
fn(10, 20)
直接调用闭包表达式:
{
(v1: Int, v2: Int) -> Int in
return v1 + v2
}(10, 20)
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
1. 完整形式:
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
2. 省略参数类型:
exec(v1: 10, v2: 20, fn: {
v1, v2 in
return v1 + v2
})
3. 省略return关键字:
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 + v2
})
4. 使用参数名简写:
exec(v1: 10, v2: 20, fn: { $0 + $1 })
5. 使用运算符:
exec(v1: 10, v2: 20, fn: +)
如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。 尾随闭包是一个被书写在函数调用括号后面的闭包表达式。
条件总结:
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
exec(v1: 10, v2: 20) {
$0 + $1
}
如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号。
func exec(fn: (Int, Int) -> Int) {
print(fn(1, 2))
}
exec(fn: { $0 + $1 })
exec() { $0 + $1 }
exec { $0 + $1 }
func sort(by areInIncreasingOrder: (Element, Element) -> Bool)
/// 返回true: i1排在i2前面
/// 返回false: i1排在i2后面
var nums = [11, 2, 18, 6, 5, 68, 45]
// 使用普通函数
func cmp(i1: Int, i2: Int) -> Bool {
// 大的排在前面
return i1 > i2
}
nums.sort(by: cmp) // [68, 45, 18, 11, 6, 5, 2]
// 使用完整闭包表达式
nums.sort(by: {
(i1: Int, i2: Int) -> Bool in
return i1 < i2
})
// 简化参数类型
nums.sort(by: { i1, i2 in return i1 < i2 })
// 简化return
nums.sort(by: { i1, i2 in i1 < i2 })
// 使用参数名简写
nums.sort(by: { $0 < $1 })
// 使用运算符
nums.sort(by: <)
// 使用尾随闭包
nums.sort() { $0 < $1 }
nums.sort { $0 < $1 }
// 结果:[2, 5, 6, 11, 18, 45, 68]
当闭包的参数不需要使用时,可以用下划线_
来忽略。
func exec(fn: (Int, Int) -> Int) {
print(fn(1, 2))
}
exec { _, _ in 10 } // 10
网上有各种关于闭包的定义,个人觉得比较严谨的定义是:
一个函数和它所捕获的变量/常量环境组合起来,称为闭包。
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
// 返回的plus和num形成了闭包
var fn1 = getFn()
var fn2 = getFn()
fn1(1) // 1
fn2(2) // 2
fn1(3) // 4
fn2(4) // 6
fn1(5) // 9
fn2(6) // 12
可以把闭包想象成是一个类的实例对象:
class Closure {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
}
var cs1 = Closure()
var cs2 = Closure()
cs1.plus(1) // 1
cs2.plus(2) // 2
cs1.plus(3) // 4
cs2.plus(4) // 6
cs1.plus(5) // 9
cs2.plus(6) // 12
func getFn() -> Fn {
var num = 0
return {
num += $0
return num
}
}
思考:如果num是全局变量呢?
如果num
是全局变量,那么就不存在捕获外层函数局部变量的情况,严格来说就不是闭包了。
typealias Fn = (Int) -> (Int, Int)
func getFns() -> (Fn, Fn) {
var num1 = 0
var num2 = 0
func plus(_ i: Int) -> (Int, Int) {
num1 += i
num2 += i << 1
return (num1, num2)
}
func minus(_ i: Int) -> (Int, Int) {
num1 -= i
num2 -= i << 1
return (num1, num2)
}
return (plus, minus)
}
let (p, m) = getFns()
p(5) // (5, 10)
m(4) // (1, 2)
p(3) // (4, 8)
m(2) // (2, 4)
等价的类实现:
class Closure {
var num1 = 0
var num2 = 0
func plus(_ i: Int) -> (Int, Int) {
num1 += i
num2 += i << 1
return (num1, num2)
}
func minus(_ i: Int) -> (Int, Int) {
num1 -= i
num2 -= i << 1
return (num1, num2)
}
}
var cs = Closure()
cs.plus(5) // (5, 10)
cs.minus(4) // (1, 2)
cs.plus(3) // (4, 8)
cs.minus(2) // (2, 4)
var functions: [() -> Int] = []
for i in 1...3 {
functions.append { i }
}
for f in functions {
print(f())
}
// 输出:
// 1
// 2
// 3
等价的类实现:
class Closure {
var i: Int
init(_ i: Int) {
self.i = i
}
func get() -> Int {
return i
}
}
var clses: [Closure] = []
for i in 1...3 {
clses.append(Closure(i))
}
for cls in clses {
print(cls.get())
}
如果返回值是函数类型,那么参数的修饰要保持统一。
func add(_ num: Int) -> (inout Int) -> Void {
func plus(v: inout Int) {
v += num
}
return plus
}
var num = 5
add(20)(&num)
print(num) // 25
注意:
(inout Int) -> Void
plus
的参数也必须是inout
类型&
来传递inout参数// 如果第1个数大于0,返回第一个数。否则返回第2个数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 20) // 10
getFirstPositive(-2, 20) // 20
getFirstPositive(0, -4) // -4
// 改成函数类型的参数,可以让v2延迟加载
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4) { 20 }
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive(-4, 20)
@autoclosure
会自动将20
封装成闭包{ 20 }
@autoclosure
只支持() -> T
格式的参数@autoclosure
并非只支持最后1个参数??
使用了@autoclosure
技术@autoclosure
、无@autoclosure
,构成了函数重载@autoclosure
的地方最好明确注释清楚:这个值会被推迟执行Swift中的空合并运算符??
就是使用了@autoclosure
:
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
这样可以避免不必要的计算:
let result = optionalValue ?? expensiveComputation()
只有当optionalValue
为nil
时,expensiveComputation()
才会被执行。
{ (参数) -> 返回值 in 函数体 }
@autoclosure
进行延迟计算