阅读视图

发现新文章,点击刷新页面。

苹果高管揭示苹果背后秘密:苹果为何不涉足搜索引擎领域?

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

在科技领域,苹果作为创新的先锋,其每一步动向都备受瞩目。

我们都知道,苹果手机在海外打开浏览器,默认使用 Google 搜索,在国内默认使用百度(也可以自己设置)。但大家有没有想过,苹果作为世界上最大的手机制造商,为什么不做自己的搜索引擎?

近期苹果高级副总裁艾迪·库(Eddy Cue)在美国华盛顿特区的联邦法院提交了一份声明,解释了为何苹果不打算像谷歌一样开发自己的搜索引擎。

image.png

看完报告之后才发现原来这一决定背后有着深刻的考量。

理由分析

艾迪·库在声明中指出,苹果反对开发搜索引擎的原因主要有以下几点:

  1. 成本与资源投入

    研发一个搜索引擎将花费苹果数十亿美元,并且需要很多年时间。这将会使得苹果不得不将投资资金和员工从公司目前专注的其他增长领域中抽离。

    今年苹果刚刚放弃计划 10 年的汽车项目,转而投入 AI,看起来已经没有更多足够的精力去专心做一个搜索引擎了。

  2. 快速变化的搜索行业

    随着人工智能的迅猛发展,搜索业务正处于快速演变之中。在此背景下,苹果认为建立一个搜索引擎在经济上存在巨大风险。

    其实这背后预示着,即使像 Google 这样的大公司,面对 AI 搜索的步步紧逼,其未来也不光明。如果不能把握住 AI,其主营的业务搜索也可能被 OpenAI 这样的新兴 AI 公司代替。

  3. 广告销售与隐私承诺

    为了创建一个“可行”的搜索引擎业务,苹果将不得不“销售定向广告”。但这并不是苹果的核心业务,而且会违背公司长久以来对用户隐私的承诺。

    苹果一直以来致力于用户隐私的保护,如果做搜索引擎,则可能会因为需要盈利而背离这一原则。

  4. 专业人员与运营基础设施的缺乏

    苹果目前没有足够的“专业化人才”和“运营基础设施”来构建和运营一个成功的搜索引擎业务。

法庭背景

前面提到了在海外,苹果手机默认搜索引擎是谷歌,因为这项合作,Google 每年要向苹果支付 200 亿美元,因此即使自己不做搜索引擎,苹果也能在这方面获得丰厚的利润,因此也就没有动力自己做一套全新的搜索引擎了。

在今年上半年的时候,美国司法部针对谷歌的反垄断审判中,法院裁定谷歌与苹果达成的将谷歌设为 Safari 默认搜索引擎的协议是非法的。对此,艾迪·库在声明中请求法院允许苹果在审判中通过自己的证人作证来为这项协议辩护。

艾迪·库在声明中写道:“只有苹果能够说明哪些未来的合作可以最好地服务于其用户。”他强调,苹果始终专注于创造最佳的用户体验,并通过与其他公司的潜在合作和安排来实现这一目标。

经济影响

艾迪·库还透露,仅在 2022 年,谷歌就向苹果支付了大约 200 亿美元作为协议的一部分。如果这一协议无法继续,库表示:“这将严重阻碍苹果继续提供最符合用户需求产品的能力。”

不过话说回来,如果最终法院裁定苹果不能再将谷歌设为 Safari 默认搜索引擎,那我觉得苹果还是有很大概率自己做一套搜索引擎的。

结语

苹果拒绝创建自己的搜索引擎,虽然有经济上的考量,更深层次的原因在于苹果对用户隐私的坚定承诺和对核心业务的专注。

这样的决策不仅反映了苹果的战略思维,也显示了其对用户体验的重视。如果你对苹果的这项决定有什么看法,欢迎在评论区分享你的观点。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

swift中Codable编码与解码源码解读

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

image.png

解码把data数据转换为Education的实例,这里decode方法的第一个参数是一个类型,返回一个实例对象,所以写成decode(Education.self, from: jsonData),Education.self表示类型本身,而Education表示对象

alt text转存失败,建议直接上传图片文件

codable

这个协议是解码和编码的组合 alt text转存失败,建议直接上传图片文件

//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协议,返回一个实例对象 alt text转存失败,建议直接上传图片文件

Decodable和Encodable本身没有什么好说的,就是把对象编码成其他格式和把其他格式解码并初始化一个实例对象的能力,里面有方法encode和decode,重要的是其中的方法遵循的协议。Eocoder和Decoder。解码时,会生成一个实例对象,对象的参数必须初始化,所以使用init()。 alt text转存失败,建议直接上传图片文件 下面是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协议,这是什么?

alt text转存失败,建议直接上传图片文件 由源码可以知道,它从对象属性得到key,映射对象属性和最终外部数据之间的对应关系。

事情还没有完,得到容器类型后呢?看看第一种,

alt text转存失败,建议直接上传图片文件 它遵循了一个协议,协议下面有编码方法encode,就得到一个想要的类型,

alt text转存失败,建议直接上传图片文件

一个完整的编码步骤应该是这样:

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)//常规
    }
}

你已经看到,我在编码时手动实现了一些东西,这是它的优势,虽然代码量大了 我可以修改:

  • 1 属性名映射(外部 key 与内部属性不同)
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 7 新 WebView:金蛇出洞,网页江湖换新天

在这里插入图片描述

概述

崇祯年间,华山派武学虽盛,却在应对江湖新局时渐显颓势;如今 SwiftUI 江湖亦是如此 ——WWDC 25 之前,若要在 SwiftUI 中显示网页,开发者恰似袁承志初闯江湖,纵有一身本领,却苦无称手兵刃。

在这里插入图片描述

直到那柄 "金蛇剑" 般的全新 WebView 横空出世,才让网页显示之道豁然开朗。

在本篇武学大家谈中,各位少侠将学到如下内容:

  • 一、往昔困局:华山旧功难破迷阵
  • 二、金蛇出洞:WWDC 25 的 WebView 新法
      1. 初窥门径:基础网页加载
      1. 内功心法:WebPage 状态管理
      1. 奇门绝技:JS 交互与平台适配
  • 三、江湖展望:金蛇之后再无钝剑

想得到那柄可以横扫武林的神兵利器金蛇剑吗?那还等什么?让我们马上开始寻“剑”之旅吧!

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反复调用时易生错乱,好比剑法中 "剑招互碍";
  • 其二,网页状态监听需另设代理,犹如练剑时还要分心护脉;
  • 其三,与 SwiftUI 状态管理结合时,常现 "内力相冲" 之象 —— 稍有不慎便会数据错乱。

在这里插入图片描述

二、金蛇出洞:WWDC 25 的 WebView 新法

正当开发者们困于旧法之时,WWDC 25 恰似一场武林大会,苹果突然亮出 "金蛇剑"——SwiftUI 原生 WebView 横空出世!此剑一出,如金蛇郎君夏雪宜重现江湖,招式灵动,浑然天成,将网页显示之道推向新境。

在这里插入图片描述

1. 初窥门径:基础网页加载

新 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 自会料理妥当。

真乃呜呼快哉!

2. 内功心法:WebPage 状态管理

若要深入掌控网页状态,需修习 "金蛇秘籍"——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的操作,可谓 "化繁为简,返璞归真"。

在这里插入图片描述

3. 奇门绝技:JS 交互与平台适配

新 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 做了两件事:

  1. 动态实时获取到了网页的标题;
  2. 将网页背景设置为金色;

此等操作,恰似金蛇郎君以金蛇锥破敌甲胄 —— 直接穿透 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-)

老司机 iOS 周报 #342 | 2025-07-14

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

新闻

Swift 宣布成立安卓工作组

Swift 官方宣布成立 Android 工作组,将 Android 列为官方支持的平台。该工作组的主要目标是为 Swift 语言添加并维护 Android 平台支持,让开发者能够使用 Swift 开发 Android 应用。

文章

🐕 A Swift Developer ’ s Guide to Prompt Engineering with Apple ’ s FoundationModels

@Barney:这篇文章是关于 Apple's FoundationModels 在 Swift 开发中的提示工程指南。Apple 的 Foundation Model 专门为 Swift 和 SwiftUI 训练,有 4096 token 的上下文限制。核心技术是使用 Generable 宏定义输出结构,通过 Guide 系统精确控制生成内容。文章强调属性顺序的重要性,因为 LLM 逐 token 生成。实用技巧包括自然语言长度修饰符、角色设定、少样本提示法和温度调节。对于用户输入,建议限制开放字段并妥善处理 guardrail 错误。为 Swift 开发者提供了原生、类型安全的 AI 集成方案。

🐕 Finding my Way

@Kyle-Ye: 独立 iOS 开发者 David Smith 分享了他在基于 iOS 26 设计语言重新设计 Pedometer++ 的地图功能的一些思考。文章详细描述了他的设计过程,包括如何让地图全屏显示、如何优化顶部按钮、如何设计浮动的信息面板等。

🐢 《别急于下定论:人工智能编程工具实则可能降低生产力》 Not So Fast: AI Coding Tools Can Actually Reduce Productivity

@Cooper Chen:在 AI 编程工具被广泛吹捧的背景下,METR 实验室通过一项严谨的随机对照试验(RCT)揭示了一个反直觉的结论:经验丰富的开发者在成熟项目中使用 AI 工具后,工作效率反而降低 19%。这项研究基于 16 位资深开源开发者在百万行代码项目中的 246 项真实任务,挑战了“ AI 必然提升效率”的行业共识。

关键发现:

  • 效率幻觉:开发者普遍预期 AI 能提速 24%,实际却拖慢 19%,认知偏差高达 40 个百分点。
  • 时间消耗:44% 的 AI 生成代码被废弃,开发者 9% 的时间用于修正 AI 输出,4% 在等待响应。
  • 适用场景:AI 在小型新项目中表现良好,但对复杂系统维护可能适得其反。

🐕 Schedule a countdown timer with AlarmKit

@阿权:文章详细介绍了如何使用 WWDC25 推出的 AlarmKit 框架实现倒计时提醒功能。过去要实现指定时间提醒功能,普通开发者只能通过苹果的通知推送。虽然通知能自定义时机甚至提醒铃声,但始终还是通知,在静音模式和专注模式下都无一幸免,要想像系统闹钟一样即使在静音和专注模式下还能提醒,只能通过新推出的 AlarmKit 了。AlarmKit 支持一次性闹钟、重复闹钟和立即开始的倒计时提醒,AlarmKit 提供的能力需要用户授权,并需要适配锁屏展示和灵动岛中的展示,具体配置可浏览原文。

只希望该功能不要被厂商滥用,尤其不要用在“加急”功能上啊!

🐎 Google I/O Extended :2025 Flutter 的现状与未来

@david-clang:本文的分享更侧重于科普类型的概括,包括 Flutter 的市场渗透率、技术进展、未来方向,其中有几个有趣的点:

  1. 市场渗透率:

    • Flutter 比 RN 的整体渗透率高:根据 2025 年 6 月腾讯端服务统计整体渗透率,Flutter 约 13%,RN 约 9%。另外,根据 Apptopia 统计,2024 年 AppStore 里 Flutter 占据所有新免费 iOS 应用的近 30%。
  2. 技术进展:

    • 线程合并:之前的 3.29 Android 和 iOS 默认合并 Dart UI 线程和平台线程,从最近 3.32 开始, Windows 和 macOS 也支持合并 Dart UI 线程和平台线程。
    • 多窗口:在本文发布后 2 天,对多窗口的支持已经合到主分支(#168728),在 Engine 层引入在 Windows、macOS 和未来 Linux 平台上创建并管理多个窗口的机制。
  3. 未来方向:

    • 移除 Cupertino 和 Material 的内置:对于 iOS 26 的液态玻璃实现,官方已经明确了不会内置支持,甚至连 Android 的最新  Material 3 Expressive 也是,长期来看,把特色控件从 Framework 内置移除,专注引擎优化,是个更好的方向。
    • ffigen/jnigen 持续优化:线程合并的主要目的,是为了抛弃历史产物 MethodChannel,而在互操作这件事情,未来肯定是 Dart 和平台语言直接互调用,而 3.32 也提到了,ffigen/jnigen 也在持续改进并内测,预计下半年会有全新的消息。

🐎 使用 Xcode 26 构建,在 (, iOS 26) 设备启动崩溃 Symbol not found: _NSUserActivityTypeBrowsingWeb

@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)

深度未来( Deep Future )给我的启发

最近我在 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 个人口可获得人口胜利。失去所有星球也会导致失败。在多人模式中,先失败的玩家可以选择在下个回合直接在当前游戏局重新开始和未失败的玩家继续游戏(但初始条件有可能比全新局稍弱)。

游戏以纯卡牌驱动,每张卡片既是行动卡,又是系统事件卡,同时卡片还是随机性的来源。抽取卡片用于产生随机性的点数分布随着游戏发展是变化的,每场战役都会向不同的方向发展,这比一般的骰子游戏的稳定随机分布会多一些独有的乐趣。

玩家每轮游戏可作最多两个独立的行动:

  • POWER 抽两张卡
  • ADVANCE 发展一项科技
  • GROW 繁殖两个人口
  • EXPAND 向临接空格移动任意数量人口,但至少在出发地留一个
  • BATTLE 和临接空格交战,或(当没有任何邻接敌人时)推进任意进度条
  • SETTLE 在有人口的区域殖民一个星球
  • EVOKE 打出一张文明卡
  • PLAN 制造一张新的指定行动卡

在执行这些行动的同时,如果玩家拥有更多的科技,就可能有更多的行动附加效果。这些科技带来的效果几乎是推进胜利进度条的全部方法,否则只有和平状态的 BATTLE 行动才能推进一格进度条。

在行动阶段之后,系统会根据玩家帝国中科技数量的多寡产生不同数量的负面事件卡。科技越发达,面临的挑战越大。但可以用手牌支付科技的维护费来阻止科技带来的额外负面事件,或用手牌兑换成商品寄存在母星和科技卡上供未来消除负面事件使用。

负面事件卡可能降低玩家的胜利进度条,最终导致游戏失败;也可能在地图增加新的野生星球及野怪。后者可能最终导致玩家失去已殖民的星球。但足够丰富的手牌以及前面用手牌制造的商品和更多的殖民星球可以用来取消这些负面事件。

每张星球卡和科技卡上都有三个空的科技栏位,在生成卡片时至少会添加一条随机科技,而另两条科技会随着游戏进程逐步写上去。

游戏达成胜利的必要条件是玩家把母星的三条科技开发完,并拥有至少一张完成的科技卡(三条科技全开发完毕),然后再满足上面提到的 6 种胜利方式条件之一:四个胜利进度条 C T S X 至少一条推进到顶,或拥有 12 区域,亦或拥有 25 人口。

胜利的玩家需要将给当局游戏的母星所在格命名,这会影响后面游戏的开局设定。同时还会根据这局游戏的胜利模式以及取得的科技情况创造出一张新的文明卡供后续游戏使用。

游戏以 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 Coffee - 一个创意工作者的线上社区

家附近有一个叫 35mm 的咖啡馆,以前经常去,某天心血来潮就想,会不会 35mm.coffee 正好是个可以注册的名呢?就上 Cloudflare 上看了下,果然可以,于是就注册了。即然注册了,就想着拿来做点什么,一开始把使用地限制在了 35MM 咖啡馆,也就是只有在那里才能使用,后来又将它改造成了只有晚上才能访问的网站,之后去掉了时间限制,改成可以随时访问的小 Twitter,还是不满意,总觉得少了一个明确的主题。

我是一个创作者,平时喜欢做点小东西,也会接触很多创作相关的内容,但总有种孤独感。回想起一开始入行的时候,学的是 ActionScript,那时经常逛一个论坛,氛围挺好的,自己写了一个什么小程序,或者学到了什么都会在那里分享,也从那里学到了不少,在交流过程中也结识了一些志同道合的朋友。

现在论坛已经成了过去式,创作者能逛的地方更多了,小红书、微博、推特、V2EX、知乎等等,但好像都没有特定主题的社区那种特有的氛围,也少了那种相对深入的、持续的、互相启发式的交流。Discord 其实蛮适合的,但它的封闭性和糟糕的桌面端是很大的问题。我开始思考,35mm.coffee 这个域名,是不是可以成为一个契机,重新找回那种失落的连接感,让它成为一个能让创作者们找到共鸣、答疑解惑、分享经验、甚至共同创作的地方?

35mm 代表着胶片时代经典的视角,一种记录真实、捕捉瞬间的工具。对于创作者而言,我们其实也正是在用自己的方式,记录着、捕捉着这个世界。

在这里,你可以:

  • 分享你的「胶片」: 无论是你刚完成的一个小项目,一段在创作中遇到的瓶颈,还是一闪而过的奇思妙想,都可以分享出来。也许你的一个困惑,正是别人曾经走过的路;你的一个灵感,也能点亮别人的火花。
  • 品味「咖啡」的醇厚: 针对某个特定主题,可以发起深入的讨论,比如「如何平衡商业与艺术创作」、「AI 工具对创作流程的影响」、「寻找个人风格的路径」等等。
  • 寻找你的「曝光」: 晒出你的作品,无论是草稿、半成品还是最终成果。
  • 建立「暗房」般的连接: 也许你会在这里找到志同道合的伙伴,一起发起一个协作项目,或者只是单纯地分享彼此的创作日常,互相打气。

最后,希望这个小站能重新找回那种失落的连接感,让创作之路不再孤单。

GESP 202506 5级真题「奖品兑换」题解

题目描述

分析

此题首先是不能暴力枚举的,因为 n 和 m 最大情况下是 10^9,这个数据规模,暴力枚举肯定会超时。

然后我们可能想到贪心,但实际可落地的贪心的策略总是有特殊情况。

最后,假如我们可以检查一个答案是否可行,我们就可以用二分答案+判定的方法求解。

二分还有一个要求,就是答案是单调递增的。我们可以想像,随着兑换券的递增,如果限定 n 的值不变,那 m 的值肯定是递增的。所以此题符合单调递增的条件。

解法

那么,对于一个可能的答案 k,我们怎么检查答案是否可行呢?

  • 我们先把 n 和 m 排序,让 n 是较大者,a 和 b 排序,让 a 是较大者
  • 对于一份奖品,可以是 n-a, m-b 来获得,也可以是 n-b, m-a 来获得,我们让 d=a-b
  • 因为 a 是较大者,所以当更换兑换方式的时候,n 的值从n-a变成了n-b,相对来说,增加了 d,m 的值减少了 d

所以:

  • 我们可以先用第一个兑换方法,把 k 个奖品换成 c1=a*k 张课堂优秀券, c2=b*k 张作业优秀券
  • 如果 c1 <=n, c2 <= m 那这个答案 k 显然就是可以的。
  • 但如果 c1 > n,我们可以想到,把超额出来的兑换换成第二个兑换方法

具体如何换呢?

  • 我们先计算超额的值,为 c1-n
  • 每次兑换可以让这个值少 d,所以需要换 r=(c1-n)/d (向上取整)r=(c1-n+d-1)/d
  • 经过如上的兑换,c1 的值减少了 d*r,c2 的值增加了 d*r

最后需要注意,因为 a*k 的范围可能超过 int,所以需要把计算过程都用 long long 来保存。

总结

此题考查了:

  • 二分+判定的解法
  • 向上取整的写法
  • 数据范围的预估
  • 时间复杂度的预估

这还是非常综合的一道题。对于没想到二分的学生,也可以用贪心或者暴力枚举骗到不少分(估计 10-15 分),所以此题也有相当的区分度,各种能力的学生都可以拿到部分分数。

详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

long long n, m, a, b, d, ans;

bool test(long long k) {
long long c1 = a*k;
long long c2 = b*k;
if (c1 > n) {
long long r = (c1 - n + d - 1) / d;
c1 -= r*d;
c2 += r*d;
}
if (c1 <= n && c2 <=m) return true;
else return false;
}

int main() {
ios::sync_with_stdio(0);
cin >> n >> m >> a >> b;
if (n < m) swap(n, m);
if (a < b) swap(a, b);
d = a - b;
long long l = 0;
long long r = n;
while (l <= r) {
long long m = (l+r)/2;
if (test(m)) {
ans = max(ans, m);
l = m+1;
} else {
r = m-1;
}
}
cout << ans << endl;
return 0;
}

构建你的“多巴胺”系统

什么是“多巴胺”系统

“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。之所以用这个隐喻,一方面是想让大家更容易理解、记忆和传播这个系统。

这个系统对我来说非常重要,它就相当于我人生的“第一性原理”一样。人类看起来是自己的主人,但人类对自身行为动机的理解很多时候并不清楚。

马斯洛把人类的需求按层次来分,在他的理论中提到的各种需求:性,安全,食物,社交,自我实现等等。但是其实,这些其实本质上,都是在为人类提供“多巴胺”。

当人类失去了“多巴胺”系统,很多时候就宁愿放弃生命:比如在战争中,很多人为了信仰而牺牲自己。这是因为他内心的目标大于活着的意义。

在实际生活中,虽然不至于放弃生命,但冒着生命危险做的事情,也不鲜见。比如消防队员救人、警察和歹徒搏斗、或者体育健儿在赛场上带伤为荣誉而战。

这些行为虽然有可能失去生命,但是换来的荣誉与成就是非常让人自豪的,可以为自己提供终身的多巴胺来源。

有人说,这个世界上只有两种生意:让人爽的生意和让人牛逼(学习、健身等)的生意。但我觉得,这都是多巴胺的生意,差别只是一个是提供短期多巴胺,一个是提供长期多巴胺。学习这种事情虽然短期很辛苦,但是收获的成就是可以提供长期的回报,从而提供长期的多巴胺。

为什么“多巴胺”系统很重要

1、人对生活的意义有需求

看看全世界有多少人信教就明白了。大部分人都需要精神上为生命的存在赋予意义。意义感会驱使人们面对挑战和困难、提供情绪支撑、获得幸福感。

在中国,很少有人信教,但是我们每一个普通人也有自己对生命的追求,哪怕是更好一点的生活,或者一个遥不可及的理想,又或者是简单地照顾好家人和孩子。

人生的目标带动着每一个人在各种重大决策的十字路口上做选择。韩寒为了赛车辍学;赵心童为了台球远赴英国;崔永远为了自由表达离开了央视;而我身边,一个亲人为了更好的照顾孩子而放弃了工作上的晋升机会。

“多巴胺”系统就是为人生的意义提供基础能量的仓库,守护好多巴胺系统,人生之路就会走得更加从容。

2、“多巴胺”系统不容易构建

我们随便看看身边,就会发现无论是学习、工作,还是退休安排和日常生活。“多巴胺”系统的构建都是非常不容易的。

2.1 学习

拿学习来说,如果将孩子的“多巴胺”系统和学校排名、升学挂钩,那么很多孩子是无法构建学习的“多巴胺”系统的。因为每个班几十个孩子,必然有排在后面 50% 的孩子。这些孩子从排名上是无法获得正向激励的。

另外,整个学习是一个不断淘汰对手的游戏。中考会淘汰 50% 的学生分流到中专,高考又会分流 50% 的人到职高,大学又会分流 90% 的学生到非重点大学。研究生考试又会分流 2/3 的本科生,只剩下 1/3。

按上面的通过率,就算你是全中国前 1% 的学生,那大概也会止步于 985/211 的研究生入学考试。

所以,在学习上,你总会有一天会遇上身边的对手都比你强,你在这个小圈子里面排在后面,如果你和同学比的话,你能收获的只有负面的情绪,感觉自己像个废物。

后面我会提到如何构建学习的多巴胺系统。

2.2 工作

也许你是一个优秀的员工,不断获得奖励和提拔,但是随着环境和年龄变化,工作中持续获得正反馈是困难的。原因如下:

第一个原因:正向激励的比例太低。只有前 20% 的员工才能获得超过其他人的回报,大部分人只能拿到普通的绩效和待遇。

第二个原因:很多工作的经验积累并不是线性的。在积累 3-5 年后,新增加的经验不足以带来相应比例产出提升,这就造成老员工工资过高,性价比不足。拿 iOS 开发来说,工作 10 年和工作 30 年的开发者的经验差异在大部分情况下表现得并不明显,这就可能造成某些工作 10 年以上的老员工薪资涨幅变慢。

第三个原因:人在 30 岁以后,体力和学习速度逐渐下降。我今年 41 岁,熬夜的能力明显变差。而我在 30 岁的时候,经常熬夜加班。工作中的一些内容如果需要的是堆时间才能完成,老员工的完成速度就不及年轻的员工。

第四个原因:岗位架构是金字塔形的。越往上需要的人越少,所以一个员工很容易最终就停在某一个岗位无法获得上升机会,背后的原因可能仅仅是因为上面已经有人了,不需要更多管理者。

2.3 退休

退休是每个人必须面对的事情,如果不做好准备,“多巴胺”系统根本就不会自己产生。因为每个人退休后,日常生活的节奏就会有巨大变化。而人的时间是需要被填满的,否则就会因为意义感缺失而产生各种问题。

2.4 其它

其它的部分还包括,生活、家庭、理财等等:

  • 对于生活:兴趣能否持续,影响“多巴胺”系统的稳定。
  • 对于家庭:如何处理夫妻关系,亲子关系,婆媳关系,都关系到多巴胺系统的稳定。
  • 对于理财:如果你买在顶峰,不但需要很长时间回本,也会承受巨大的账面亏损压力,给自己的多巴胺系统带来巨大冲击
  • 对于伤痛:个人对伤痛,特别是心理层面上的伤痛处理也很重要,心理上的伤痛如果处理不好,就像应激的小猫一样,会给身体带来严重的伤害。

如何构建“多巴胺”系统

接下来,我就讲讲我对各种情况下构建“多巴胺”系统的心得。

1、对于学习

对于学习,我们需要刻意设计“多巴胺时刻”。让原来可能没有的多巴胺变得有,让原来分泌得少的多巴胺,变得分泌多。具体来说,我们可以:

一、定期回顾,肯定自己的进步。我每年都会写年度总结,之前觉得每年没有什么变化,但是总结的时候,发现还是有挺多进步的,这样就让自己更有成就感。

二、设立奖励,自我颁奖。不管是小的学习还是大的学习,都可以设立奖励。我在做竞赛题的时候,之前做完我就继续做下一题。但后来我发现,如果我每次做对,都挥舞一下手臂小小庆祝一下,就会开心很多。所以,即便是很小的自我肯定,都可以让多巴胺给我们更多激励。

三、适当分享,获得亲朋鼓励。人是社会动物,自己的成就还是要适当分享出来。但是对自己友谊不深的朋友就没太有必要,有可能会造成人家妒忌,或者人家会认为你是一个喜欢炫耀的人,没必要。

四、构建无限游戏,不要设置终点和上限。学习无止境,如果我们可以一直设立目标,就可以无限玩下去。对于生命来说,能够无限玩的游戏不多,学习算是一个。

2、对于工作

刚刚说过,随着环境和年龄变化,工作中持续获得正反馈是困难的。所以,对于工作,我们首先需要做的是降低预期。工作首先你是获得持续现金流的谋生手段;它如果能够给你持续的正向激励,当然很好,但是如果有一天,工作无法给你带来正反馈,那么你也可以就把它当作一份工作即可。

在工作上不要讲太多回报,公平。很多事情做了没有结果,但是公司付你钱了,所以你认真把事情做好,就很好,也很专业。

另外,在工作上,我们也需要尊重规律,做累进的事情。坚持在自己的专业领域积累经验,如果自己的年龄大了或者行业发展不好,也要接受工资不再上涨这些现实。

在工作上,我们还可以尝试杠铃策略,即:同时拥有两个不太相关的专业技能。通过在业余时间利用自己的爱好或者特长来发展副业,如果万一出现什么变动,自己的副业就可以成为主业,保证自己不至于失业。

3、对于退休

退休是人一辈子重要的规划之一,也是人生活法的重大转换。

对于退休,最重要的事情就是让提前规划好兴趣,让兴趣填满自己的时间。否则,人生一下子多了那么多时间,很容易觉得无聊。

这个兴趣最好是无限挑战游戏。这样可以几十年也做不完。

这个兴趣也最好可以锻炼到身体(例如:广场舞、摄影、骑行之类)。

最后,退休还有一个很重要的事情:要管好自己的钱,不冒大的风险,不折腾高风险的投资。因为挣太多钱自己也不一定能花完,但是如果亏很多就会影响自己的退休生活。

4、日常生活

日常生活中,有这些技巧可以带来更多的多巴胺:

一、主动给生活带来变化

我自己的经验是,主动做一些以前没做过的事情,会给生活带来新鲜感。比如:

  • 我家每过几年就会尝试换个房子租,每次都有不同的体验。
  • 每年出游几次,每次去不同的地方,让自己开眼界。
  • 购物,看上什么东西就买买买。
  • 庆祝。为自己的成绩庆祝,为朋友的成绩庆祝,为家人的成绩庆祝。

二、自立

不要太依赖别人,或者太依赖于某个工作,或者将自己放到一个困境,或者太陷入一个目标。这不是说我们应该不努力。对于生活,我们应该全情投入,把过程做好;但是对于结果,我们应该顺其自然。

三、终身学习

学习是少有的,可以持续给人带来获得感的事情。而且这个事情是没有终点的,属于一种“无限游戏”,这就让我们永远不会觉得无聊。

我最近因为兴趣又开始学习编程,遇到一个算法没看懂,我就慢慢想,可能想个一周,甚至两周,我感觉这才是一个学习的状态,就是慢慢的,不紧不慢的,学完一个再学下一个。

相对来说,学校的学习更像是一个工业化的人才产出器,每个人需要像机器一样在指定的时间学习完指定的内容,但是每个人的学习能力是不一样的,其实对每个人来说,匹配自己的学习速度才是最佳的学习方案。

四、关注过程,弱化结果

人生是一场体验,并非要留下什么,也留不下什么。

如果我们想想 100 年后谁能记得我们,我们会发现结论是:没有人。即使是自己的亲人,过了三代你可能也不会记得。大家可以想想,你知道你的爷爷的爷爷叫什么名字,长什么样,做过什么成绩吗?就算你记得,你的孩子以后会记得吗?

所以,如果人生到最后不会有任何人记得我们,那么我们人生的意义是什么?我认为核心的意义就是人生本身。就像《活着》中写道:活着就是最大的意义。

对于人生这种重过程,无结果的“游戏”,我们活在当下,关注过程,把自己的人生过好,就是一个非常棒的事情了。别的更多的结果,我们做不到,也没有什么意义。

5、对于家庭

对于家庭,最简单的获得多巴胺的方式是:低预期。比如:

对于家人,不要指望家人一定要为自己付出。家人能够不让你付出,就是超预期。有这样的心态,你每天都是超预期。

对于孩子也一样,低预期,不鸡娃。

  • 孩子小的时候,我们只需要尽量培养孩子兴趣,兴趣是最大的老师,对于结果,则需要看孩子的天赋和运气,所以我们只能静待花开。
  • 当孩子成年后,她会有自己的生活,作为父母也应该降低预期,孩子能活成什么样,最主要的还是靠孩子自己。
  • 当我们老了后,也别指望孩子给自己养老,不啃老就不错了。有这样的低预期,也容易每天获得超预期的结果。

6、对于朋友

我认为有三种朋友,可以给我们提供持续的多巴胺。

  • 一种朋友是相互帮助、支持的人。显然你们相互会收获很多。
  • 一种是可以给你提供指导的前辈,牛人。你可以收获到成长。
  • 一种是你可以给别人提供指导的后辈。你可以收获到成就感。

那哪些是消耗你多巴胺的朋友呢?

  • 每次需要你的时候找你,但你需要他的时候总逃避的人。
  • 和你话不投机,没有共同语言的人。
  • 无法平等对话的人,有可能是对方太过强于你,懒得和你对话;也可能是对方太弱于你,你懒得和他对话。
  • 让你感觉到有压力,但是除了消耗你多巴胺外,并不能给你带来任何其他好处的人。
  • 你讨厌的人。
  • 你嫉妒的人。

我有些时候,有点讨好型人格,就是不喜欢一个人,也不愿意和人家起冲突,很多时候碍于面子还是淡淡地交往。后来我发现这样不对,这完全是一种对多巴胺系统的伤害,想到这些我就主动断开了一些不喜欢的朋友的来往。其实有一些人是很优秀的,但是多巴胺系统为先的决策,让我还是会坚决断开联系。

7、对于伤痛

小孩子如果反复盯着糖果看,最后就会忍不住吃掉糖果。如果有人伤害了你,你反复回忆这个伤害的过程,你就会受到更多的内心部分的伤害。

著名作家蔡澜最近去世了,别人问他,他的爱人离他而去了,他是如何克服下来的。蔡澜说:你如果老去想这件事情,你就会发疯,所以我尽量让自己不去想这件事情。

芒格和巴菲特的公司之前特别看好一个接班人,后来这个接班人做了一些违背公司原则的事情,在收购一家公司前,自己私下提前买了这家公司的股票,自己获利了几百万美元。事情暴露之后,这个接班人辞职了。别人问芒格怎么看这个事情。

面对欺骗与背叛,芒格说:永远不要责备自己,永远不要有受害者心态。当你产生这种心态的时候,只会让你自己难受,不会带来任何其它正面的影响,因此你不应该花时间去感受它,哪怕是一秒钟。所以,更应该的心态是应对这种情况,为未来的不确定性做好准备。

芒格最后总结道:“I am not a victim. I am a survivor.”

所以,站在建立“多巴胺”系统的角度,任何只有负面效果的情绪都是不值得去强化和感受的。如果你忍不住,你可以尽量不去想它。更好的办法是像芒格那样,有一个更加强大的幸存者视角来看待所有的坏运气、灾难、欺骗与背叛。让这些负面情绪不影响自己的多巴胺系统。

8、不内耗和自恰

我后来发现,其他人讲的一些行事原则,在表达角度上虽然不一样,其实也是一样的道理。比如我们讲的“不内耗”原则。

内耗就是一种持续消耗“多巴胺”的心理行为。如果以构建“多巴胺”系统作为人生准则的话,我们会发现内耗没有任何效果。当我们面对不如意的时候,要么改变,要么适应,要么淡化,而内耗是一种既不改变,又不适应,又反复强化负反馈的行为。百害而无一利。

自恰的底层含义是:所有事情能够自圆其说,不矛盾,不冲突,自然也就不内耗了,不消耗多巴胺。

所以,人需要活得“自恰”,只有自恰才能睡好觉,持续获得多巴胺。

主观与客观

“多巴胺”系统有主观的部分,也有客观的部分。

一、主观部分

“多巴胺”系统对于个人内心是一种主观行为和感受,而不是一种客观描述和标准。所以,对于芒格来说,一个重要朋友的背叛不是对“多巴胺”系统的冲击;但换一个人,可能觉得天塌了,一辈子再难信任他人。

因此,我们更应该调整的是自我的行事方式和思考问题的角度,而不是改变其他人。我们可以远离那些影响我们“多巴胺”系统的人和事,但是当坏运气到来的时候,我们只能接受。

二、客观部分

当然,“多巴胺”系统在指导我们行为的时候,是让我们客观上在做具体的行为选择。通过行为选择让我们尽可能构建有利于我们产生多巴胺的外界环境。比如我刚刚提到的:提前规划退休生活、选择终身学习、多搞庆祝活动等。这些有利的环境不但不会消耗我们主观意志来维护多巴胺,还会给我们提供愉悦,贡献多巴胺。

小结

“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。我们通过:

  • 主观上,调整自己的思考和看待事情的方式
  • 客观上,搭建好能够持续供养自己多巴胺的外部环境

利用“多巴胺”系统,让自己的人生少一点内耗,少一点纠结,多一点平静,多一点快乐。

愿每个读者都能过好当下的每一天,谢谢!

Flutter 弹窗解析:从系统 Dialog 到完全自定义

在移动端 UI 设计中,弹窗(Dialog) 是承载「打断式沟通」(interrupt communication) 的核心控件:它能在适当时机抓住用户注意力,提示风险、请求确认或引导后续操作。下面我沿着 「系统 → 半自定义 → 全自定义」 的脉络,简单说明在Flutter里面写出可扩展的弹窗。

本文完整Demo代码: github.com/wutao23yzd/… 中的Demo6

效果如下所示:

1 系统级弹窗:一行代码就能用

1.1 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 自带标题、正文、按钮插槽,适合提示 + 二次确认的场景。

1.2 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 让你能在调用处直接拿到用户选择。


2 系统 BottomSheet:从屏幕底部滑入

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 自适应高度。
  • 典型场景:iOS 风格 ActionSheet、文件操作菜单等。

3 迈向完全自定义

系统组件虽方便,但在品牌一致性、复杂交互、动画细节上往往力不从心。在Demo的自定义方案中,拆成 数据层 + 动效层 + API 层

3.1 数据模型: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 把「危险高亮」逻辑封装内部,外部不用再判红色。

3.2 动效控件:Tappable

class Tappable extends StatefulWidget {
  const Tappable.faded({ required this.child, this.onTap, FadeStrength fadeStrength = FadeStrength.md });
  ...
}
  • 点击反馈 做成独立组件,不污染业务代码。
  • AnimationController + FadeTransition 实现轻量「按下变暗 / 松手复原」的Material 触感
  • 提供普通 (normal) 与半透明 (faded) 两种模式,可在不同场景复用。

3.3 API 层:BuildContext 扩展

方法 用途 关键点
showAdaptiveDialog 包一层 AlertDialog.adaptive,自动适配 iOS / Android 风格 同时暴露 titleTextStyle,便于定制
showBottomModal showModalBottomSheet 包装 内置圆角 & DragHandle,可自由开关
showListOptionsModal 带滚动的选项列表 组合 ModalOption + Tappable;点击后 Navigator.pop 将选项回传
showImagePreview 圆形图片预览弹窗 AspectRatio + DecorationImage 让图片保持清晰

把重复的 shape / safeArea / isDismissible 等参数收拢在扩展方法中——调用端只关注内容和交互,让业务代码最小化

3.4 真实场景示例

3.4.1 媒体发布菜单

OutlinedButton(
  child: const Text('自定义选项弹窗'),
  onPressed: () {
    context.showListOptionsModal(
      title: '新建',
      options: createMediaModalOptions(...),
    ).then((option) => option?.onTap(context));
  },
);

3.4.2 圆形图片预览

context.showImagePreview('https://picsum.photos/id/237/300/200');
  • Dialog 背景透明 (backgroundColor: Color(0x00000000)),中间圆形头像带描边。
  • 手势关闭后自动回到原界面,适合社交 App 查看大图。

3.4.3 设置页选项

context.showListOptionsModal(
  options: [
    ModalOption(child: const LocaleModalOption()),
    ModalOption(child: const ThemeSelectorModalOption()),
    ModalOption(child: const LogoutModalOption()),
  ],
);

每个 ModalOption 里直接塞自定义子组件,如 DropdownButton 语言/主题选择,实现**「边选边生效」**的即时交互体验。


4 小结

  1. 分层设计:把「数据模型」「交互动效」「对外 API」解耦,可复用也易维护。
  2. 动画小细节 (Tappable):让自定义弹窗拥有系统级触感,提升专业度。
  3. 危险操作显色:像 distractiveColor 这样封装「红色」逻辑,可避免遗漏、风格不一致。

通过这些实践,基本能在 Flutter 中构建出自定义且易于迭代的弹窗体系,真正让「对话」成为强化用户体验的利器。

写在最后:本文代码有参考www.youtube.com/watch?v=xr5…

iOS防截屏实战

一、背景

1、由于系统限制,iOS无法禁止用户的截屏行为;只有在发生截屏时触发了一个截屏通知-- UIApplication.userDidTakeScreenshotNotification

2、但是这个通知只起到告知作用,收到这个通知时,截屏已经发生了,截屏的内容会以图片的方式存到相册;

3、从系统特性层面来看,似乎无法限制iOS防截屏;

4、即然系统无法限制截屏,那我们就想办法修改截屏的内容呗!!!让截屏截了个寂寞😂

二、收益

  • 1、像我们做数据安全的公司,有些数据不希望用户随便就能分享出去,防截屏就挺不错的;
  • 2、一些设计类的App,比如美图,设计的图片在你没开会员前,也不允许随便分享出去,防截屏也很有用;
  • 3、当然,用另一个手机拍照咱就没招了,防截屏也是防君子不防“小可爱”;

三、技术方案

基于 UITextField 的安全文本输入特性isSecureTextEntry)以及 私有视图层级的利用,从而在截屏或录屏时隐藏敏感内容。

现成的第三方库:github.com/RyukieSama/…

pod 'RyukieSwifty/ScreenShield'

ScreenShield的用法很简单,我就不介绍了,有兴趣的自己玩儿。

四、技术细节

1、首先在UIViewController的loadView方法中设置 self.viewScreenShieldView

2、然后布局在self.view上的子视图都会加到安全图层上,整个页面就具有了防截屏功能;

3、当用户截屏时,截屏出来的将是一个空白页面,这样就起到了防截屏,防录屏的作用;

五、问题分析

真正使用过程中会遇到如下问题:

问题一:self.view.subviews数组存放的子视图不是真正添加到self.view上的子视图?,就是说无法通过self.view.subviews获取子视图数量了

查看ScreenShieldView的原码可以发现,虽然我们在loadView中将self.view设置成了ScreenShieldView,并且是通过self.view添加的子视图,但其实所有子视图都添加到了ScreenShieldViewsafeZone上了。

分析问题

想重写subviews方法?可以试一试!

public override var subviews: [UIView]{
    guard let safe = safeZone else {
        return super.subviews
    }
    return safe.subviews
 }

运行发现所有添加到self.view上的子视图都不显示了

  • 1、subviews不止开发者会调用,UIView内容系统也会调用,重写了subviews后系统调用的将是重写后的函数,并且返回的将是safe.subviews;
  • 2、所有子视图实际是被添加到safeZone上的,但是设置的约束都是与self.view的。重写subviews函数后,约束设置会失效;
  • 3、所以重写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后的图层

image.png

1、可以看到ViewController上的图层self.view就是ScreenShieldView图层; 2、self.view的子视图是添加在_UITextLayoutCanvasView上的,即safeZone;

再来看看ScreenShieldView的safeZone是添加在哪里的

image.png

分析代码发现,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系统特性的基础上,按业务需求进行取舍的过程。

Demo

15-错误处理

Swift 错误处理 (Error Handling)

📚 目录

📖 内容概述

错误处理是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)")
    }
}

do-catch语句

详细的错误捕获
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)")
    }
}

try?和try!

try? - 可选的错误处理
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)")
}
try! - 强制错误处理
func forcedOperation() {
    // try! 假设操作不会失败,如果失败则程序崩溃
    let result = try! divide(10, by: 2)  // 5.0
    print("结果: \(result)")
    
    // 危险的用法 - 如果失败会导致程序崩溃
    // let badResult = try! divide(10, by: 0)  // 运行时崩溃
}

defer语句

清理资源
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) ?? ""
}
多个defer语句
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
    }
}

最佳实践

1. 错误类型设计
// 好的做法:使用枚举定义相关错误
enum DatabaseError: Error {
    case connectionFailed
    case queryFailed(String)
    case dataCorrupted
    case timeout
}

// 避免:使用通用错误类型
// struct GenericError: Error { let message: String }
2. 错误信息
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 "输入格式不正确"
        }
    }
}
3. 错误处理策略
// 策略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)")
}

📝 重要提示

  1. 错误类型:实现 Error 协议,通常使用枚举
  2. 函数声明:使用 throws 标记可能抛出错误的函数
  3. 错误抛出:使用 throw 关键字抛出错误
  4. 错误处理:使用 trydo-catch 语句处理错误
  5. 资源清理:使用 defer 语句确保资源被正确释放
  6. 错误传播:错误会自动向上传播直到被捕获

🎯 总结

Swift的错误处理机制提供了一种类型安全且表达力强的方式来处理运行时错误。通过合理使用错误处理,我们可以:

  • 编写更健壮的代码
  • 提供更好的用户体验
  • 更容易调试和维护代码
  • 避免程序意外崩溃

掌握错误处理是编写高质量Swift代码的重要技能。


本文档基于Swift 5.0+版本,涵盖了错误处理的核心概念和最佳实践。

9.方法

方法

目录

  1. 实例方法
  2. self的使用
  3. mutating方法
  4. @discardableResult
  5. 类型方法

实例方法

实例方法(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的使用

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

通常情况下,不需要显式地写出self,Swift会自动推断。但在以下情况下需要使用:

  1. 参数名与属性名相同时
  2. 闭包中访问实例属性时
  3. 方法返回self时
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方法

值类型(结构体、枚举)的实例方法默认不能修改实例的属性。如果需要修改,必须使用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关键字
  • mutating方法可以为self赋予一个全新的实例
struct Point {
    var x = 0.0, y = 0.0
    
    mutating func moveToOrigin() {
        self = Point(x: 0.0, y: 0.0)
    }
}

枚举中的mutating方法

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

@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()              // 忽略返回值,不会产生警告

什么时候使用@discardableResult

  • 方法既可以用于获取值,也可以用于执行操作
  • 调用者有时关心返回值,有时不关心
  • 避免编译器产生"未使用的返回值"警告
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 vs class

  • 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

在类型方法中,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:当前实例(实例方法)或类型(类型方法)的引用

最佳实践

  1. 实例方法:用于操作实例数据,提供实例相关的功能
  2. 类型方法:用于工厂方法、工具方法、类型相关的操作
  3. mutating方法:值类型需要修改自身时使用
  4. @discardableResult:既可以用于获取值也可以用于执行操作的方法
  5. 链式调用:方法返回self,支持链式编程风格

使用建议

  • 优先使用实例方法处理实例相关的逻辑
  • 使用类型方法提供工厂方法或工具函数
  • 值类型修改自身时记得使用mutating
  • 合理使用@discardableResult避免编译器警告
  • 理解self的不同含义,正确使用

7.闭包

闭包

闭包表达式

在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: +)

尾随闭包

如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。 尾随闭包是一个被书写在函数调用括号后面的闭包表达式。

条件总结:

  1. 闭包表达式是最后一个实参就可以使用尾随闭包

基本用法

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 }

示例 - 数组的排序

Array的sort方法

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是全局变量,那么就不存在捕获外层函数局部变量的情况,严格来说就不是闭包了。


练习

练习1:多个闭包共享变量

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)

练习2:闭包数组

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 }

使用@autoclosure

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

getFirstPositive(-4, 20)

@autoclosure特点

  • @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()

只有当optionalValuenil时,expensiveComputation()才会被执行。


总结

闭包的核心概念

  1. 闭包表达式{ (参数) -> 返回值 in 函数体 }
  2. 简写形式:从完整形式到运算符,逐步简化
  3. 尾随闭包:提高代码可读性
  4. 闭包定义:函数 + 捕获的环境变量
  5. 内存模型:类似于类的实例对象

使用场景

  • 函数式编程
  • 异步回调
  • 数组操作(sort、map、filter等)
  • 延迟计算
  • 自定义控制流

最佳实践

  1. 能简写的地方尽量简写,提高代码简洁性
  2. 使用尾随闭包提高可读性
  3. 注意闭包的捕获机制,避免循环引用
  4. 合理使用@autoclosure进行延迟计算
  5. 理解闭包的内存模型,帮助优化性能

6.结构体和类

结构体和类

结构体

在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。

比如BoolIntDoubleStringArrayDictionary等常见类型都是结构体。

struct Date {
    var year: Int
    var month: Int
    var day: Int
}
var date = Date(year: 2019, month: 6, day: 23)

所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)。 其实也就是说上面的代码其实等效下面的代码

struct Date {
    var year: Int
    var month: Int
    var day: Int
    init(year: Int, month: Int, day: Int) {
        self.year = year
        self.month = month
        self.day = day
    }
}

在第6行调用的,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)。


结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值

思考:下面代码能编译通过么?

struct Point {
    var x: Int?
    var y: Int?
}
var p1 = Point(x: 10, y: 10)
var p2 = Point(y: 10)
var p3 = Point(x: 10)
var p4 = Point()

答案:可以编译通过

原因:可选项都有个默认值nil,因此可以编译通过。


自定义初始化器

一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器。

struct Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

结构体内存结构

struct Point {
    var x: Int = 0
    var y: Int = 0
    var origin: Bool = false
}
print(MemoryLayout<Point>.size)      // 17
print(MemoryLayout<Point>.stride)    // 24
print(MemoryLayout<Point>.alignment) // 8
  • size: 实际占用的内存大小
  • stride: 分配的内存大小(包含对齐)
  • alignment: 内存对齐

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。

struct Point {
    var x: Int = 0
    var y: Int = 0
}
let p1 = Point()
let p2 = Point(x: 10, y: 20)  // 结构体可以
let p3 = Point(x: 10)         // 结构体可以
let p4 = Point(y: 20)         // 结构体可以
class Point {
    var x: Int = 0
    var y: Int = 0
}
let p1 = Point()              // 类只能这样
// let p2 = Point(x: 10, y: 20)  // 编译错误

类的初始化器

如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器。

成员的初始化是在这个初始化器中完成的。

方式一:默认值

class Point {
    var x: Int = 10
    var y: Int = 20
}
let p1 = Point()

方式二:自定义初始化器

class Point {
    var x: Int
    var y: Int
    
    init() {
        x = 10
        y = 20
    }
}
let p1 = Point()

上面2段代码是完全等效的。


结构体与类的本质区别

结构体是值类型(枚举也是值类型),类是引用类型(指针类型)

class Size {
    var width = 1
    var height = 2
}

struct Point {
    var x = 3
    var y = 4
}

func test() {
    var size = Size()
    var point = Point()
}

image.png


值类型

值类型赋值给varlet或者给函数传参,是直接将所有内容拷贝一份。

类似于对文件进行copy、paste操作,产生了全新的文件副本。属于深拷贝(deep copy)

struct Point {
    var x: Int
    var y: Int
}

func test() {
    var p1 = Point(x: 10, y: 20)
    var p2 = p1
    
    p2.x = 11
    p2.y = 22
    // 请问p1.x和p1.y是多少?
    // 答案:p1.x = 10, p1.y = 20(不受影响)
}

image.png

值类型的赋值操作示例

var s1 = "Jack"
var s2 = s1
s2.append("_Rose")
print(s1) // Jack
print(s2) // Jack_Rose

var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]

var d1 = ["max": 10, "min": 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1) // ["other": 7, "max": 10, "min": 2]
print(d2) // ["max": 12, "min": 2]

性能优化:

  • 在Swift标准库中,为了提升性能,StringArrayDictionarySet采取了Copy On Write的技术
  • 比如仅当有"写"操作时,才会真正执行拷贝操作
  • 对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
  • 建议:不需要修改的,尽量定义成let

自己的总结

  • 结构体和类的本质是,结构体是值类型,而结构体是引用类型
  • 值类型赋值给var,let或者参数传参,是直接将所有内容都拷贝,类似于文件直接进行copy一份出来,产生了全新的文件副本。属于深拷贝。
  • 而且swift中绝大多数的公开类型都是结构体,比方说Bool、Int、Double、 String、Array、Dictionary这些都是结构体,这些直接使用 = 赋值就会产生一个新的副本

值类型的赋值操作

struct Point {
    var x: Int
    var y: Int
}

var p1 = Point(x: 10, y: 20)
p1 = Point(x: 11, y: 22)

内存变化:

  • 赋值前:p1.x = 10, p1.y = 20
  • 赋值后:p1.x = 11, p1.y = 22

image.png

引用类型

引用赋值给varlet或者给函数传参,是将内存地址拷贝一份。

类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)

class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

func test() {
    var s1 = Size(width: 10, height: 20)
    var s2 = s1
    
    s2.width = 11
    s2.height = 22
    // 请问s1.width和s1.height是多少?
    // 答案:s1.width = 11, s1.height = 22(受影响)
}

image.png

对象的堆空间申请过程

在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:

  1. Class.__allocating_init()
  2. libswiftCore.dylib_swift_allocObject_
  3. libswiftCore.dylibswift_slowAlloc
  4. libsystem_malloc.dylibmalloc

注意:

  • 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数
  • 通过class_getInstanceSize可以得知:类的对象至少需要占用多少内存
class Point {
    var x = 11
    var test = true
    var y = 22
}

var p = Point()
class_getInstanceSize(type(of: p))  // 40
class_getInstanceSize(Point.self)   // 40

引用类型的赋值操作

image.png

内存变化:

  • 原对象被销毁,s1指向新的对象
  • 栈空间地址改变,堆空间创建新对象

值类型、引用类型的let

image.png

let 就是代表p当前的内存不能修改,对于结构体来说p来说 x,y 都在p的内存中,p实际是占16 个字节,代表这16字节都不能修改 而对于对象来说,代表 s是常量, s的内存其实就是一个指针常量,只代表对象的地址不能改变,但是对面里面的width ,height 都不存在s中,是存在s指向的对象的堆里面,不是存在p这个变量里面,所以s不能修改,但是s里面的东西是可以改的

嵌套类型

就是说枚举可以嵌套在结构体内。

struct Poker {
    enum Suit: Character {
        case spades = "♠", hearts = "♥", diamonds = "♦", clubs = "♣"
    }
    
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
    }
}

print(Poker.Suit.hearts.rawValue)  // ♥

var suit = Poker.Suit.spades
suit = .diamonds

var rank = Poker.Rank.five
rank = .king

方法定义

枚举、结构体、类都可以定义方法。

class Size {
    var width = 10
    var height = 10
    
    func show() {
        print("width=\(width), height=\(height)")
    }
}

let s = Size()
s.show()  // width=10, height=10
struct Point {
    var x = 10
    var y = 10
    
    func show() {
        print("x=\(x), y=\(y)")
    }
}

let p = Point()
p.show()  // x=10, y=10
enum PokerFace: Character {
    case spades = "♠", hearts = "♥", diamonds = "♦", clubs = "♣"
    
    func show() {
        print("face is \(rawValue)")
    }
}

let pf = PokerFace.hearts
pf.show()  // face is ♥

重要概念:

  • 一般把定义在枚举、结构体、类内部的函数,叫做方法
  • 方法占用对象的内存么? 不占用
  • 方法的本质就是函数
  • 方法、函数都存放在代码段
  • 类里面的函数和最外层的函数本质没有啥区别,仅仅是作用域不同而已

结构体和类的存放位置问题

结构体变量的位置取决于他在哪里创建的,如果是对象里面创建,那么在堆空间,如果在函数里面创建,肯定是在内存空间。如果在最外层生命的,那么他就在全局区(数据段)

对于类里面,不论在那创建,指针变量指向的对象一定在堆空间,而指针变量如果是在函数里面定义的就在栈空间。 Pasted Graphic 7.tiff

其他笔记补充

1.arm64 架构里面

1.指针变量占8个字节 2. int 类型占 8个字节

2.对象里面在堆空间里面需要?
  1. 指向类型信息
  2. 引用计数
  3. 里面的存储对象成员变量的内存
3.判断是不是堆空间?

看看有没有调用 alloc malloc方法

4.si 什么作用?

可以跟进去 finish 调试的作用结束当前的调试,跳到下一个 端点

Swift 5.9 新特性揭秘:非复制类型的安全与高效

这里每天分享一个 iOS 的新知识,快来关注我吧

image.png

前言

在 Swift 中,类型默认是可复制的。这种设计简化了开发过程,因为它允许值在赋值给新变量或传递给函数时轻松复制。

然而,这种便利有时会导致意想不到的问题。例如,复制单次使用的票据或重复连接数据库可能会导致无效状态或资源冲突。

为了解决这些问题,在 Swift 5.9 中引入了非复制类型。通过将类型标记为~Copyable 来实现,我们可以显式地阻止 Swift 复制它。

这保证了值的唯一所有权,并施加了更严格的约束,从而降低了出错的风险。接下来让我们详细了解一下非复制类型。

非复制类型的示例

以下是一个非复制类型的简单示例:

struct SingleUseTicket: ~Copyable {
    let ticketIDString
}

与常规值类型的行为不同,当我们将非复制类型的实例分配给新变量时,值会被移动而不是复制。如果我们尝试在稍后使用原始变量,会得到编译时错误:

let originalTicket = SingleUseTicket(ticketID: "S645")
let newTicket = originalTicket

print(originalTicket.ticketID) // 报错 'originalTicket' used after consume

需要注意的是,类不能被声明为非复制类型。所有类类型仍然是可复制的,通过保留和释放对对象的引用来实现。

非复制类型中的方法

在非复制类型中,方法可以读取、修改或消费self

借用方法

非复制类型中的方法默认是借用borrowing 的。这意味着它们只能读取实例,允许安全地检查实例而不影响其有效性。

struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    func describe() {
        print("This ticket is \(ticketID).")
    }
}

let ticket = SingleUseTicket(ticketID: "A123")

// 打印 `This ticket is A123.`
ticket.describe()

可变方法

可变方法mutating 提供了对self 的临时写访问,允许在不使实例无效的情况下进行修改。

struct SingleUseTicket: ~Copyable {
    var ticketID: String

    mutating func updateID(newID: String) {
        ticketID = newID
        print("Ticket ID updated to \(ticketID).")
    }
}

var ticket = SingleUseTicket(ticketID: "A123")

// 打印 `Ticket ID updated to B456.`
ticket.updateID(newID: "B456")

消费方法

消费方法consuming 接管self 的所有权,一旦方法完成就使实例无效。这对于完成或处置资源的任务非常有用。在调用方法后,任何尝试访问实例的操作都会导致编译错误。

struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    consuming func use() {
        print("Ticket \(ticketID) used.")
    }
}

func useTicket() {
    let ticket = SingleUseTicket(ticketID: "A123")
    ticket.use()
    
    ticket.use() // 报错 'ticket' consumed more than once
}

useTicket()

需要注意的是,我们不能消费存储在全局变量中的非复制类型,因此在我们的示例中我们将代码包装在useTicket() 函数中。

非复制类型在函数参数中的应用

当将非复制类型作为参数传递给函数时,Swift 要求我们为该函数指定所有权模型。我们可以将参数标记为借用borrowing、输入输出inout 或消费consuming,每种标记提供不同级别的访问权限,类似于类型内部的方法。

借用参数

借用所有权允许函数临时读取值,而不消耗或修改它。

func inspectTicket(_ ticket: borrowing SingleUseTicket) {
    print("Inspecting ticket \(ticket.ticketID).")
}

输入输出参数

输入输出参数inout 提供了对值的临时写访问,允许函数修改它,同时将所有权返回给调用者。

func updateTicketID(_ ticketinout SingleUseTicket, to newID: String) {
    ticket.ticketID = newID
    print("Ticket ID updated to \(ticket.ticketID).")
}

消费参数

当一个参数被标记为消费时,函数完全接管该值的所有权,使其对于调用者无效。例如,如果我们有一个消费方法,我们可以在函数中使用它,而无需担心在函数外部使用该值。

func processTicket(_ ticket: consuming SingleUseTicket) {
    ticket.use()
}

析构函数和丢弃操作符

非复制结构体和枚举可以像类一样拥有析构函数deinit,它们会在实例生命周期结束时自动运行。

struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    
    deinit {
        print("Ticket deinitialized.")
        
        // 清理逻辑
    }
}

然而,当一个消费方法和一个析构函数都执行清理时,可能会有冗余操作的风险。为了解决这个问题,Swift 引入了丢弃操作符discard

通过在消费方法中使用discard self,我们可以显式阻止调用析构函数,从而避免重复逻辑:

struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    
    consuming func invalidate() {
        print("Ticket \(ticketID) invalidated.")
        
        // 清理逻辑
        
        discard self
    }
    
    deinit {
        print("Ticket deinitialized.")
        
        // 清理逻辑
    }
}

另外需要注意的是,只有当我们的类型包含可轻松销毁的存储属性时,才能使用discard。不能包含引用计数、泛型。

总结

最近几年,swift 出了很多新特性,非复制类型是其中之一,实际开发中,非复制类型很少用到,但是了解这些特性,可以让我们在开发中更加得心应手。随着 Swift 的不断发展,这些类型代表了语言在性能和正确性方面的重大进步。

但是这些越来越复杂的特性也让 swift 初学者望而却步,希望这篇文章能帮助大家了解非复制类型,在实际开发中,如果需要使用非复制类型,可以参考这篇文章。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

iOS Block

  1. block 本质上是是一个oc对象,内部也有isa指针。这个对象内部封装了函数调用地址以及函数调用环境(参数参数、返回值、捕获的外部变量)

  2.  int age = 20;
     void (^block)(void) = ^{
     nslog(@"age is %d",age)
     }
     
     struct __main_block_impl_0 {
     struct __block_impl impl;
     struct __main_block_desc_9 *desc
     int age
     }
    
  3.  int c = 1000; // 全局变量
     static int d = 10000; // 静态全局变量
     
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
     
             int a = 10; // 局部变量
             static int b = 100; // 静态局部变量
             void (^block)(void) = ^{
                  NSLog(@"a = %d",a);
                  NSLog(@"b = %d",b);
                  NSLog(@"c = %d",c);
                  NSLog(@"d = %d",d);
              };
              a = 20;
              b = 200;
              c = 2000;
              d = 20000;
              block();
         }
         return 0;
     }
     
     // ***************打印结果***************
     2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
     2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
     2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
     2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000
    
  4. 全局变量不会捕获,直接访问

  5. 静态局部变量,捕获的是变量的地址,所以在block外面修改值以后,也会改变

  6. 普通变量,会直接捕获。外面在修改值,block内部是新生成了一个变量,不改变值

  7. _NSGlobalBlock_如果一个block里面没有访问普通局部变量,也就是没有捕获任何值,就是这种global类型,存在数据区。继承链:_nsgloableblock_ :nsblock:nsobject

  8. 如果一个block里面访问了普通局部变量,那他就是一个_nsstackblock_,他在内存中存放在栈区,特点是其释放不受开发者控制,都是系统操作,如果对他惊醒了copy,就会把这个block复制到堆上。

如何在 visionOS 上使用 MPS 和 CIFilter 实现特殊视觉效果

说明

在 visionOS 开发中,视觉效果一直都是开发的一个难点。尽管苹果推出了 ShaderGraph 来简化 Shader 的开发,在此基础上我开源了 RealityShaderExtension 框架来帮助降低 Shader 开发的门槛,但在实际开发中,我们仍然面临两个问题:

  • 数学与几何知识要求太高,难以开发出满意的效果
  • 某些效果如 高斯模糊GaussianBlur直方图Histogram 单纯依靠 ShaderGraph 难以编写的,且运行效率不佳

image.png

UnityMaterial.gif

苹果针对 ShaderGraph 功能不够强大的弱点,给出的解决方案是:使用 LowLevelTexture + Compute Shader 更加灵活的实现各种算法功能,然而手写 Metal Compute Shader 代码依然是非常困难的。

不过,苹果有一个已经高度优化的 Compute Shader 框架:Metal Performance Shaders ,我们可以直接与 LowLevelTexture 一起使用。

同时,经过研究,在 UIKit 中常用的 CIFilter 图片处理框架,也是可以与 LowLevelTexture 一起使用的,这样就无需再手动编写各种算法代码了。

同时,不仅是图片可以处理,视频也可以继续使用 AVPlayer 播放的同时,添加 MPS/CIFilter 进行处理。

图片处理

对图片处理时,MPS 和 CIFilter 的基本步骤是一样的:

  • 处理流程: MPS/CIFilter -> LowLevelTexture -> TextureResource -> UnlitMaterial

Image(MPS)

使用 MPS 进行处理时:

  • 只需要通过 commandBufferLowLevelTesxture 中获取目标纹理 outTexture
  • 将源纹理和目标纹理传递给 MPS filter 即可。

关键代码如下:

func populateMPS(inTexture: MTLTexture, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

    // Set up the Metal command queue and compute command encoder,
    .....

    // Create a MPS filter.
    let blur = MPSImageGaussianBlur(device: device, sigma: model.blurRadius)

    // set input output
    let outTexture = lowLevelTexture.replace(using: commandBuffer)
    blur.encode(commandBuffer: commandBuffer, sourceTexture: inTexture, destinationTexture: outTexture)

    
    // The usual Metal enqueue process.
    .....
}

ScreenRecording_06-28-2025 16-38-15_1.2025-07-02 14_31_36.gif

Image(CIFilter)

使用 CIFilter 进行处理时:

  • 需要根据 outTexturecommandBuffer 创建一个 CIRenderDestination
  • [可选] 为了更好与 Metal 协作,最好创建一个 GPU-Based CIContext
  • [可选] 如果遇到颜色空间显示不正确,可以设置 options 中 .workingColorSpace 为 sRGB 等。
  • 最后调用 ciContext.startTask 将处理后的图片写入 CIRenderDestination 中。

关键代码如下:

let blur = CIFilter(name: "CIGaussianBlur")

func populateCIFilter(inTexture: MTLTexture, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

    // Set up the Metal command queue and compute command encoder,
    .......
    
    // Set the CIFilter inputs
    blur?.setValue(CIImage(mtlTexture: inTexture), forKey: kCIInputImageKey)
    blur?.setValue(model.blurRadius, forKey: kCIInputRadiusKey)

    // set input output
    let outTexture = lowLevelTexture.replace(using: commandBuffer)
    let render = CIRenderDestination(mtlTexture: outTexture, commandBuffer: commandBuffer)

    // Create a Context for GPU-Based Rendering
    let ciContext = CIContext(mtlCommandQueue: commandQueue,options: [.cacheIntermediates: false, .workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!])

    if let outImage = blur?.outputImage {
        do {
            try ciContext.startTask(toRender: outImage, to: render)
        } catch  {
            print(error)
        }
    }

    // The usual Metal enqueue process.
    ......
}

ScreenRecording_06-28-2025 16-38-15_1.2025-07-02 14_32_15.gif

视频处理

视频处理要稍微复杂一些,需要创建 AVMutableVideoComposition 来从 AVPlayer 中获取视频帧信息再进行处理,处理后的视频继续在 AVPlayer 中直接播放,也可以另外导出到 LowLevelTexture 中进行显示。

注意:视频处理在老版本的(即 Xcode 16.4 中原始的) Vision Pro 模拟器中不能正常工作,在新的模拟器“Apple Vision Pro 4K” 中 使用 CIFilter 处理后的颜色显示不正确。不过在真机测试中,都是正常的。

Video(CIFilter)

  • 处理流程:[ CIFilter + AVMutableVideoComposition + AVPlayerItem ] -> VideoMaterial

好消息是,苹果针对 CIFilter 有一个简单方案:

  • 在创建 AVMutableVideoComposition 时创建一个闭包
  • 在闭包中通过 AVAsynchronousCIImageFilteringRequest 获取适合 CIFilter 处理的视频帧数据
  • 源视频数据直接传给 CIFilter 处理后,重新写入 AVAsynchronousCIImageFilteringRequest 即可播放出模糊后的视频。
let asset: AVURLAsset....


let playerItem = AVPlayerItem(asset: asset)

let composition = try await AVMutableVideoComposition.videoComposition(with: asset) { request in
    populateCIFilter(request: request)
}
playerItem.videoComposition = composition


// Create a material that uses the VideoMaterial
let player = AVPlayer(playerItem: playerItem)
let videoMaterial = VideoMaterial(avPlayer: player)

真正的处理代码也非常简单,将 CIFilter 的输出重新写入到 request 中即可:

let ciFilter = CIFilter(name: "CIGaussianBlur")

func populateCIFilter(request: AVAsynchronousCIImageFilteringRequest) {
    let source = request.sourceImage
    ciFilter?.setValue(source, forKey: kCIInputImageKey)
    ciFilter?.setValue(model.blurRadius, forKey: kCIInputRadiusKey)

    if let output = ciFilter?.outputImage {
        request.finish(with: output, context: ciContext)
    } else {
        request.finish(with: FilterError.failedToProduceOutputImage)
    }
}

ScreenRecording_06-28-2025 16-38-15_1.2025-07-02 14_27_20.gif

Video(MPS)

  • 处理流程: [ MPS + AVMutableVideoComposition + AVPlayerItem ] -> LowLevelTexture -> TextureResource -> UnlitMaterial

通过 MPS 来处理视频要更加复杂一些:

  • 我们需要自定义一个 customVideoCompositorClass ,赋值给 AVMutableVideoComposition
  • 实现它的协议方法,指定输入和输出的像素格式
  • startRequest() 中获取视频帧并转换为 MTLTexture ,由 MPS 进行处理
  • [可选] 将源视频写入回去,这样就能在 AVPlayer 中继续播放源视频

自定义一个 SampleCustomCompositor,并赋值给 composition.customVideoCompositorClass

let composition = try await AVMutableVideoComposition.videoComposition(withPropertiesOf: asset)
composition.customVideoCompositorClass = SampleCustomCompositor.self

let playerItem = AVPlayerItem(asset: asset)
playerItem.videoComposition = composition

SampleCustomCompositor 需要指定我们需要的视频帧像素格式,然后就可以在 startRequest() 中获取到对应格式的视频帧,进行模糊处理。

class SampleCustomCompositor: NSObject, AVVideoCompositing {
    .....
    // 指定我们需要的视频帧格式。一定要设置 kCVPixelBufferMetalCompatibilityKey,否则与 Metal 会出现兼容性问题,导致黑屏等
    var sourcePixelBufferAttributes: [String: any Sendable]? = [
        String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_32BGRA],
        String(kCVPixelBufferMetalCompatibilityKey): true // Critical! 非常重要
    ]
    // 我们处理后返回的视频帧格式
    var requiredPixelBufferAttributesForRenderContext: [String: any Sendable] = [
        String(kCVPixelBufferPixelFormatTypeKey):[kCVPixelFormatType_32BGRA],
        String(kCVPixelBufferMetalCompatibilityKey): true
    ]

    ....


    func startRequest(_ request: AVAsynchronousVideoCompositionRequest) {

        .....

        let requiredTrackIDs = request.videoCompositionInstruction.requiredSourceTrackIDs
        let sourceID = requiredTrackIDs[0]
        let sourceBuffer = request.sourceFrame(byTrackID: sourceID.value(of: Int32.self)!)!

       
        Task {@MainActor in
            // 将模糊后的视频输出到 LowLevelTexture 中
            populateMPS(sourceBuffer: sourceBuffer, lowLevelTexture: SampleCustomCompositor.llt!, device: SampleCustomCompositor.mtlDevice!)
        }
        // 保持原视频继续输出
        request.finish(withComposedVideoFrame: sourceBuffer)
    }


    @MainActor func populateMPS(sourceBuffer: CVPixelBuffer, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

        .....

        // Now sourceBuffer should already be in BGRA format, create Metal texture directly
        var mtlTextureCache: CVMetalTextureCache? = nil
        CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &mtlTextureCache)

        let width = CVPixelBufferGetWidth(sourceBuffer)
        let height = CVPixelBufferGetHeight(sourceBuffer)
        var cvTexture: CVMetalTexture?
        let result = CVMetalTextureCacheCreateTextureFromImage(
            kCFAllocatorDefault,
            mtlTextureCache!,
            sourceBuffer,
            nil,
            .bgra8Unorm,
            width,
            height,
            0,
            &cvTexture
        )
        let bgraTexture = CVMetalTextureGetTexture(cvTexture)
  
        // Create a MPS filter with dynamic blur radius
        let blur = MPSImageGaussianBlur(device: device, sigma: Self.blurRadius)
 
        // set input output
        let outTexture = lowLevelTexture.replace(using: commandBuffer)
        blur.encode(commandBuffer: commandBuffer, sourceTexture: bgraTexture, destinationTexture: outTexture)

        // The usual Metal enqueue process.
        ....
    }
}

使用 customVideoCompositorClass + MPS ,可以在 AVPlayer 输出源视频(下图左)的同时,在 LowLevelTexture 中输出模糊后的视频(下图右): ScreenRecording_06-28-2025 16-38-15_1.2025-07-02 14_26_31.gif

参考

项目完整示例:github.com/XanderXu/MP…

参考资料:

Swift 6.2:江湖再掀惊涛浪,新功出世震四方

在这里插入图片描述

概述

江湖代有才人出,各领风骚数百年。

自 Swift 语言横空出世,便在 iOS 开发的武林中搅动风云。如今 WWDC 25 之上,Apple 闭门三年炼就的《Swift 6.2 真经》终见天日,书中所载新功个个精妙绝伦,足以让开发者们的代码功力更上一层楼。

在这里插入图片描述

在本篇武林秘闻中,各位少侠将领悟到如下奥义:

  1. 并发迷局终得解:"nonisolated (nonsending)" 与 "defaultIsolation" 双剑合璧 1.1 第一式:"nonisolated (nonsending)" 调和阴阳 1.2 第二式:"defaultIsolation" 定鼎乾坤
  2. "@concurrent" 破界令牌:独辟蹊径的旁门绝学
  3. 内存管理新心法:InlineArray 与 Span 的 "缩骨功" 3.1 InlineArray:栈上藏兵,招之即来 3.2 Span:内存视图,隔岸观火
  4. 严格内存安全的 “金钟罩”:让内存错误无处遁形
  5. 语言互操新经脉:Swift 与 C++ "打通任督二脉"
  6. 与 Java 的跨界合作
  7. 结语:新功在手,江湖我有

今日便由老夫为诸位少侠拆解其中奥秘,且看这新功如何改写江湖格局。 Let's go!!!;)


1. 并发迷局终得解:"nonisolated (nonsending)" 与 "defaultIsolation" 双剑合璧

往昔江湖,并发编程堪称开发者的 "鬼门关"。

多少英雄好汉在此折戟沉沙 —— 同步函数与异步函数如同正邪两道,运行规则大相径庭;主 Actor 调用时更是冲突不断,轻则编译器怒目相向,重则数据走火入魔,当真令人闻风丧胆。

Swift 6.2 携 "nonisolated (nonsending)" 与 "defaultIsolation" 两大神功而来,恰似倚天屠龙双剑合璧,专破这经脉错乱之症。

1.1 第一式:"nonisolated (nonsending)" 调和阴阳

此功专为理顺函数调用的 "阴阳二气" 所创。

标注此功的函数,既能保持独立姿态(nonisolated),又不会随意发送数据(nonsending),恰似一位守礼的侠客,既不依附门派,又不轻易出手伤人:

// 旧制:异步函数常因隔离问题"走火入魔"
actor DataManager {
    var value: Int = 0
    
    // 欲调用此函数,需先过编译器"三关"
    func fetchData() async -> Int {
        return value
    }
}

// 新功:nonisolated (nonsending) 让函数"独善其身"
actor DataManager {
    var value: Int = 0
    
    nonisolated(nonsending) func fetchData() async -> Int {
        // 可安全访问非隔离数据,或执行独立逻辑
        return 42
    }
}

这般写法,如同给函数戴上 "君子剑",既保持独立风骨,又不伤邻里(其它 Actor),实乃解决隔离冲突之必备良药。

1.2 第二式:"defaultIsolation" 定鼎乾坤

此功堪称主 Actor 门派的 "盟主令"—— 可在包级别(package)定下规矩:凡未明确 "叛逃" 的代码,皆默认归入主 Actor 麾下

这便如武林盟主昭告天下:"未投他派者,皆听我号令",瞬间省去无数手动标注的繁琐:

// 在Package.swift中启用盟主令
swiftSettings: [
    .defaultIsolation(MainActor.self), // 立主Actor为盟主
    .enableExperimentalFeature("NonisolatedNonsendingByDefault")
]

// 此后代码自动归入主Actor,无需再画蛇添足
class ViewModel {
    func updateUI() async {
        // 默认为主Actor内运行,可安心操作UI
        print("UI更新无虞")
    }
}

有了这盟主令,UI 相关代码自归其位,后台任务也能各安其职,当真 "物各有主,井然有序"。

在这里插入图片描述

2. "@concurrent" 破界令牌:独辟蹊径的旁门绝学

江湖之大,总有需要 "特立独行" 之时。

Swift 6.2 推出的 "@concurrent" 令牌,恰如一张 "通关文牒",持此令牌者,可脱离调用者的经脉,另辟新的隔离语境,堪称 "破界而行" 的旁门绝学。

此令牌虽威力无穷,却有铁律约束:仅能授予 "nonisolated" 函数

若将其用于 Actor 门派的招式,除非该招式已明确 "脱离门派"(标注 nonisolated),否则便是 "违规练功",日后必遭编译器反噬:

// 正道:nonisolated函数持令牌,名正言顺
actor NetworkClient {
    @concurrent
    nonisolated func fetchImage() async throws -> UIImage {
        // 脱离NetworkClient的隔离,另起炉灶
        let data = try await URLSession.shared.data(from: url)
        return UIImage(data: data.0)!
    }
}

// 禁忌:未脱离门派却持令牌,必遭反噬
actor NetworkClient {
    @concurrent // 编译器怒喝:"此等叛逆,当诛!"
    func fetchImage() async throws -> UIImage {
        // 此乃禁忌招式,万万不可学
    }
}

这令牌的妙用在于:当你需要一个 "临时工"(独立隔离的函数),又不想让它沾染主 Actor 的 "门派气息" 时,只需授予此令,便能让其 "独来独往,自成一派"。

3. 内存管理新心法:InlineArray 与 Span 的 "缩骨功"

内存管理向来是秃头少侠们的 "内功根基",根基不牢地动山摇,招式再花也难成大器。

Swift 6.2 推出的 InlineArray 与 Span,恰似两套 "缩骨功",能将内存占用压缩到极致,运行速度却如离弦之箭。

在这里插入图片描述

3.1 InlineArray:栈上藏兵,招之即来

寻常数组(Array)如同 "堆上营寨",虽容量可观,却需耗费时间搭建(堆内存分配)。

InlineArray 则是 "栈上藏兵",固定大小,随用随取,省去了营寨搭建的功夫:

// 声明一个可容纳5名"士兵"(Int)的栈上营寨
var inlineArray: InlineArray<Int, 5> = [1, 2, 3, 4, 5]

// 直接取用,无需等待营寨搭建
inlineArray[2] = 100
print(inlineArray) // [1, 2, 100, 4, 5]

此功最适合 "小股特种暗杀部队"(固定大小、数量不多的数据),如游戏中的坐标点、传感器的实时数据等,调用时快如闪电,绝不拖泥带水。

3.2 Span:内存视图,隔岸观火

Span 堪称 "内存望远镜"—— 它不持有内存,仅提供一片连续内存的 "视图",既能安全访问,又不占用额外空间,恰似隔岸观火,知全局而不添柴也:

let buffer: [UInt8] = [0x01, 0x02, 0x03, 0x04]

// 用望远镜观察内存,从索引1开始,看3个元素
let span = buffer[1..<4].withSpan { $0 }
print(span.count) // 3
print(span[0]) // 0x02

在解析二进制数据(如网络协议、文件格式)时,Span 能让你 "按图索骥",无需复制数据即可精准操作,实乃 "事半功倍" 之法。

4. 严格内存安全的 “金钟罩”:让内存错误无处遁形

内存安全问题一直是 iOS 开发中的一个 “心腹大患”,稍有不慎就可能导致程序崩溃、数据丢失等严重后果。

Swift 6.2 引入了严格内存安全特性,就像是为程序穿上了一层坚固的 “金钟罩”,能够有效地抵御各种内存错误的侵袭。

在以往的开发中,指针操作、内存分配与释放等操作常常隐藏着许多危险,少侠们需要花费大量的精力去确保内存的正确使用。而现在,启用严格内存安全特性后,编译器会对代码进行更加严格的检查,一旦发现潜在的内存安全问题,就会及时发出警告。

在这里插入图片描述

例如,在 Xcode 中,我们可以在项目的构建设置中将 “Strict Memory Safety” 设置为 “yes” 来启用这一特性。重新构建后,编译器会仔细检查代码中的每一处内存操作,如是否存在悬空指针、是否有内存泄漏等问题。

在这里插入图片描述

如果发现问题,编译器会给出详细的错误提示,帮助微秃少侠们及时修复,就像在江湖中,有了一位明察秋毫的武林前辈时刻提醒我们招式中的破绽,让我们能够及时修正,避免陷入东方不败的危险境地。

5. 语言互操新经脉:Swift 与 C++ "打通任督二脉"

江湖之中,门派林立,Swift 与 C++ 便如两大武学世家,各有精妙却隔阂甚深。

Swift 6.2 新修的 "互操经脉",终于让两派高手得以 "切磋武艺,互通有无"。

在这里插入图片描述

如今在 Swift 中调用 C++ 代码,恰似 "少林高僧学武当太极",招式转换自然流畅:

// C++中的"铁砂掌"函数
// int strike(int strength, int times);

// Swift中直接施展,无需翻译
import CppMartialArts

let damage = strike(100, 3) // 调用C++函数,如探囊取物
print("造成伤害:\(damage)")

更妙的是,C++ 的类也能在 Swift 中 "返璞归真",仿佛戴上 "易容面具",外观是 Swift 类,内里却是 C++ 的筋骨:

// C++的"Sword"类在Swift中可用
let mySword = CppSword(length: 1.2)
mySword.sharpen() // 调用C++方法
let damage = mySword.cut(target: "enemy")

这般互通,恰似武林大会上各派高手同台竞技,取长补短,当真 "海纳百川,有容乃大"。

6. 与 Java 的跨界合作

Swift 6.2 还为与 Java 的互操作性提供了更好的支持。

在这里插入图片描述

在一些跨平台开发场景中,Swift 与 Java 的交互需求日益增长。现在,Swift 6.2 使得 Swift 代码与 Java 代码之间的通信和协作变得更加容易,仿佛在两个不同的武林世界之间搭建了一座坚固的桥梁。

例如,在某些需要与 Java 后端服务进行交互的 iOS 应用中,Swift 6.2 的新特性可以帮助开发者更高效地实现数据传输和功能调用,大大提升了开发效率,让微秃少侠们能够在不同语言的 “江湖” 中自由穿梭,实现更强大的应用功能,并且希望少掉几根头发。

7. 结语:新功在手,江湖我有

Swift 6.2 的诸位新功,或解并发之困,或强内存之基,或通语言之隔,恰如为开发者打通了 "任督二脉",从此代码之路再无阻塞。

江湖路远,挑战常新,但只要手握这些新功秘籍,便能 "运筹帷幄之中,决胜千里之外"。诸位少侠,何不即刻闭关修炼,待功成之日,便是横行代码江湖之时!

在这里插入图片描述

记住,真正的高手,从不困于招式,而是善用利器。Swift 6.2 这柄神兵已交你手,接下来,便看你如何在开发的江湖中,写下属于自己的传奇!

感谢各位秃头少侠们的观赏,我们青山不改、绿水长流,江湖再见、后会有期!8-)

苹果内购IAP(一) Transaction 数据含义

以下是你提供的 StoreKit 2 Transaction JSON 各字段含义解析:

字段 类型 含义
transactionId String 本次交易的唯一标识符(Apple 服务器生成)。每次用户购买或续订时都不同。
originalTransactionId String 原始交易 ID。对于自动续订订阅,首次购买时生成,后续续订会复用该 ID,用来关联同一订阅链。
webOrderLineItemId String 用于 App Store 后端报表的行项目 ID,可用于跨平台(如 Web、iOS)或者后台对账。
bundleId String App 的 Bundle Identifier,表示是哪一个应用发起了这笔交易。
productId String 购买的内购商品标识符(Product Identifier),即你在 App Store Connect 中配置的 ID。
subscriptionGroupIdentifier String 订阅组 ID,属于同一组的订阅产品互斥,同组内同一用户只能激活一个订阅方案。
purchaseDate Number 本次交易的购买时间,Unix 毫秒数(UTC)。例如 1752148768000 表示 2025‑12‑10 08:19:28 UTC。
originalPurchaseDate Number 原始购买时间,Unix 毫秒数。对于续订,将是首次购买的时间。
expiresDate Number 订阅或试用期的到期时间,Unix 毫秒数。到期后如果未续订,就视为订阅结束。
quantity Int 购买的数量。对于订阅通常为 1,消费型商品可能大于 1。
type String 内购类型,此处 "Auto-Renewable Subscription" 表示自动续订订阅;其他可能值还有 "Non-Consumable""Consumable""Non-Renewing Subscription"
deviceVerification String Base64 编码的设备验证令牌,用于防篡改校验,可发送到服务器并与 Apple 校验。
deviceVerificationNonce String 随机生成的唯一值,与 deviceVerification 配合使用,保证验证请求的唯一性。
inAppOwnershipType String 购买归属类型:
"PURCHASED":用户已购买
"FAMILY_SHARED":通过家庭共享获得
"UNKNOWN":未知或无权访问
signedDate Number 交易数据在设备上签名的时间,Unix 毫秒数,用于校验数据新鲜度。
environment String 交易环境:
"Sandbox":沙盒测试
"Production":正式环境
transactionReason String 触发此次交易的原因:
"PURCHASE":用户主动购买
"RENEWAL":自动续订
"INTERACTIVE_RENEWAL":用户在订阅管理界面手动续订等
storefront String App Store 区域码(ISO 国家/地区简码),如 "CHN" 表示中国大陆。
storefrontId String App Store 地区标识符,用于更精细的区域分发。
price Int 以最小货币单位表示的价格。例如 59000 表示 ¥590.00(CNY)。
currency String 货币代码,如 "CNY""USD"
appTransactionId String App 级别的交易 ID,可用于客户端内部追踪。同一个 transactionId,可能在不同平台(iOS/macOS)下有不同 appTransactionId

💡 小贴士

  • 时间字段 (purchaseDateexpiresDate 等) 都是以毫秒为单位的 Unix 时间戳,需根据时区转换展示给用户。
  • 服务器校验:通常你会将 deviceVerification + deviceVerificationNonce 或整个 JSON 发送到后台,由后台调用 Apple 验证接口进行二次校验,确保证据未被篡改。
  • 续订关联originalTransactionId 贯穿同一次订阅链,方便后台做续订统计和账单归因。

以上字段含义能帮助你在客户端和后台实现精确的订阅状态管理与数据对账。 🚀

type 字段一般都有什么样是值

type 字段对应了内购项目的种类,在 StoreKit 2 的 Transaction JSON 中,你常见到以下几种值:

  • Consumable
    消耗型商品,一次购买后即“用完”,可重复购买(如游戏内金币、道具)。
  • Non-Consumable
    非消耗型商品,一次购买永久有效(如解锁专业版功能、永久皮肤)。
  • Non-Renewing Subscription
    非自动续期订阅,到期后需要用户手动续订(如半年课程访问权限)。
  • Auto-Renewable Subscription
    自动续期订阅,到期后由系统自动续费(如月度/年度会员)。

transactionReason 一般都有什么值,含义什么

transactionReason 表示触发此次交易的原因,在 StoreKit 2 的 Transaction JSON 中,常见的取值有三种:

  • PURCHASE
    用户主动发起的首次购买或在 App 内直接购买订阅时产生的交易。Apple Developer
  • RENEWAL
    自动续订订阅到期后,由系统在后台自动发起的续订交易。Apple Developer
  • INTERACTIVE_RENEWAL
    用户在 App Store 的订阅设置界面(或通过 App 内提供的入口)主动续订已过期或已取消的订阅时产生的交易。Stack Overflow

📌 小贴士

  • 这三种原因涵盖了 用户购买系统自动续订用户手动续订 的主要场景。
  • 当你在 Transaction.updates 或者通过 App Store Server API 查询到交易 JSON 时,可以根据 transactionReason 字段判断当前是首次购买、自动续订,还是交互式续订,以便做不同的业务逻辑处理(如提示用户“已续订”或“请手动续订”)。
❌