阅读视图

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

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

这里每天分享一个 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-)

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 字段判断当前是首次购买、自动续订,还是交互式续订,以便做不同的业务逻辑处理(如提示用户“已续订”或“请手动续订”)。

WWDC 25 风云再起:SwiftUI 7 Charts 心法从 2D 到 3D 的华丽蜕变

在这里插入图片描述

概述

在 iOS 开发这个波谲云诡的江湖中,SwiftUI 可谓是一位后起之秀,以其简洁明快的招式迅速在 UI 框架领域中崭露头角。

在这里插入图片描述

而其中的 Charts 框架,更是如同江湖中的 “数据可视化宝典”那样,让各位秃头少侠们能够轻松将复杂的数据转化为直观易懂的图表。

在本篇武林秘籍中,列位少侠将会领悟如下招式:

    1. 江湖起源——原有 Charts 框架的武学修炼
    1. 江湖变革——SwiftUI Charts 3D 图表来袭
    1. 轻功飞行——SurfacePlot 打造梦幻曲面图
    1. 握剑诀——交互与视角
    1. 3D 图表修炼的注意事项
    • 5.1 避免数据过载
    • 5.2 谨慎选择图表类型
    • 5.3 性能优化不容忽视
    1. 尾声:3D 图表开启数据可视化新纪元

那还等什么呢?各位初入江湖的豪客和“大虾”们,请随老夫一起进入图表的美妙世界吧!Let's go!!!;)


1. 江湖起源——原有 Charts 框架的武学修炼

何曾几时,WWDC 2020(SwiftUI 4.0,iOS 16)江湖风起云涌,数不清的英雄豪杰和兵器纷繁复杂,数据可视化在其中占据了举足轻重的地位。

而正是在这样的背景下,SwiftUI 的 Charts 框架横空出世,犹如一位初出茅庐的少年剑客,虽然招式简单,却也灵活迅捷,迅速得到了武林各大门派的认可。

在这里插入图片描述

初入江湖的菜鸟侠客们可以通过它迅速绘制出柱状图、折线图和饼状图等基础武学,简单易懂,入门容易。记得那年,当 18 岁的列位秃头少侠们在代码中添加 Chart 时,心中便涌起一股非凡的成就感,仿佛宝子们也成为了数据江湖中的一员。

如下便是一个简单的柱状图的代码示例,犹如初入武林的少年,凭借一把利剑便能出奇制胜:

import SwiftUI
import Charts

struct SimpleBarChart: View {
    let data: [Double] = [2, 3, 5, 7, 11, 13]

    var body: some View {
        Chart(data) { value in
            BarMark(x: .value("Value", value))
        }
        .chartXAxis {
            AxisMarks()
        }
    }
}

这就是“基本剑法”,以简洁利落见长,正如初出茅庐的剑客,刚刚踏入这个江湖那样的飘逸洒脱:

在这里插入图片描述

随着修炼的深入,少侠们会逐渐意识到,这种图表只能为大家在江湖中打下些许基础,但它也暴露出了一些不足。

在这里插入图片描述

虽然二十步之内,便可知敌人风吹草动,然而一旦对手修炼到更高的境界,单靠这种平凡的武学便显得不足以应对复杂的数据纷争。

2. 江湖变革——SwiftUI Charts 3D 图表来袭

谁能想象,江湖中的一场风云变幻,竟会让 Charts 框架焕发新生。

在WWDC 25 上,苹果总舵主为这门熟悉的武功奥义又注入了新的活力,推出了 Chart3D。这不再是寻常的剑法,而是进入了三维的殿堂。就像一位绝世大侠,早已超越了平面世界的束缚,开始在三维空间中闪展腾挪。

在这里插入图片描述

这时的 Chart3D,犹如一位有了深厚内力的高手,能在三维空间中挥洒自如。宝子们不再仅仅是直线和曲线的过客,而是能在空间中将数据点、曲面、视角交织成一幅立体图景。

无论是点的组合,还是面的铺展,或是交互的旋转,每一处都透露着数据与现实世界的紧密联系。

快看这段代码,仿佛是大侠抬手之间,便可将复杂的数据织入眼前的三维世界那样畅快淋漓:

struct ContentView: View {
    let data = [(2,5), (3,7), (5,11), (7,8), (11,20), (13,10)]

    var body: some View {
        NavigationStack {
            Chart3D(data.indices, id: \.self) { index in
                let item = data[index]
                PointMark(
                    x: .value("X", item.0),
                    y: .value("Y", item.1),
                    z: .value("Z", index)
                )
                .foregroundStyle(.red.gradient)
            }
            .chartXAxisLabel("X轴")
            .chartYAxisLabel("Y轴")
            .chartZAxisLabel("Z轴")
            .navigationTitle("Charts 心法展示")
        }
    }
}

此时,数据不仅停留在纸面上,而是跃然于三维空间之中,犹如剑客挥舞长剑,刺破苍穹:

在这里插入图片描述

现在,列为微秃小少侠们可以随心所欲地操控每一个数据点的位置,仿佛掌握了整个空间的节奏,就问你们赞不赞呢?

3. 轻功飞行——SurfacePlot 打造梦幻曲面图

在武林中,总有一些高手以轻盈如风的身法著称,他们步伐矫健,时隐时现。而 SurfacePlot 就如同这般,能够以平滑的曲面将两个变量之间的关系展现得淋漓尽致。

在这里插入图片描述

它能让小伙伴们不再拘泥于线条和点,而是化繁为简,把复杂的数据关系化作一张优美的曲面,轻盈地在三维空间中随意漂浮。

如果宝子们想描绘一条像“乾坤大挪移”般自由流畅的曲线,那便可以借助 SurfacePlot 来用数学函数得偿所愿。

下面的代码犹如一套行云流水的剑法,将数学的深奥与图形的简洁相结合,点滴之间尽显工艺之精妙:

import SwiftUI
import Charts

struct SurfacePlotChart: View {
    var body: some View {
        NavigationStack {
            Chart3D {
                SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in
                    let h = hypot(x, z)
                    return sin(h) / h * 2
                }
                .foregroundStyle(.heightBased)
            }
            .chartXScale(domain: -10...10)
            .chartZScale(domain: -10...10)
            .chartYScale(domain: -0.23...10)
            .navigationTitle("Chart3D 心法传功")
        }
    }
}

这就像是武林中的一招“飞燕回旋”,优雅、流畅,同时又极具威力:

在这里插入图片描述

当各位微秃侠客们把数学函数转化为三维曲面,它就不再只是抽象的公式,而是化身为一场精彩的武林争斗,令人叹为观止。

4. 握剑诀——交互与视角

然而,江湖变幻莫测,如何在纷繁复杂的数据中保持清醒的视角,便成为了每个剑客面临的严峻挑战。

Chart3D 的强大之处不仅仅在于它能够描绘出三维的世界,更在于它提供了丰富的交互功能,能够根据需要调整视角,让宝子们从不同角度观看同一数据的图表,仿佛随时可以恣意改变剑法招式那般美妙。

在这里插入图片描述

以下代码示范了如何通过手势控制图表的旋转角度,并且设置初始的视角与相机投影,这种灵活性犹如大侠挥剑的自由度,让小伙伴们在数据的世界里遨游无碍:

struct ContentView: View {
    @State var data = [(Int,Int)]()
    
    private func createData() {
        for _ in 0...99 {
            let x = Int.random(in: 0...100)
            let y = Int.random(in: 0...100)
            data.append((x, y))
        }
    }
    
    @State private var pose = Chart3DPose(
        azimuth: .degrees(20),    // 水平角度
        inclination: .degrees(15) // 垂直角度
    )

    var body: some View {
        NavigationStack {
            Chart3D(data.indices, id: \.self) { index in
                let item = data[index]
                PointMark(
                    x: .value("X", item.0),
                    y: .value("Y", item.1),
                    z: .value("Z", index)
                )
                .foregroundStyle(.red.gradient)
            }
            .task {
                createData()
            }
            .chartXAxisLabel("X轴")
            .chartYAxisLabel("Y轴")
            .chartZAxisLabel("Z轴")
            .chart3DPose($pose)
            .navigationTitle("Charts 心法展示")
        }
    }
}

宝子们可以像大侠操控剑气那般,调整图表的角度与视野,每一次变化,都能带来全新的观感与体验:

在这里插入图片描述

同样我们略施小计,之前的曲面图也可以如法炮制:

在这里插入图片描述

从此,数据的世界也因此变得不再单调,而是充满了无限可能。

5. 3D 图表修炼的注意事项

5.1 避免数据过载

虽然 3D 图表能够展示丰富的数据信息,但在使用时也要注意避免数据过载。过多的数据点或过于复杂的数据维度,会让图表变得混乱不堪,就像武林高手在战斗中面对过多的敌人,反而会陷入困境。开发者需要对数据进行筛选和精简,突出重点,确保图表清晰可读性。

5.2 谨慎选择图表类型

不同的图表类型适用于不同的数据展示场景,在使用 3D 图表时,要根据数据的特点和分析目的,谨慎选择合适的图表样式。

例如,3D 柱状图适合用于对比数据,3D 散点图适合分析数据之间的关系,而 3D 饼图则不太适合这些场景,因为在三维空间中,饼图的角度和比例可能会让人产生视觉误解。

选择合适的图表类型,就像武林高手选择了合适的兵器,才能发挥出最大威力。

5.3 性能优化不容忽视

由于 3D 图表的渲染和计算量较大,容易对应用的性能产生影响。因此,在开发过程中,要注重性能优化。

可以采用异步加载数据、使用高效的数据结构和算法、合理利用缓存等方法,确保图表的加载和交互丝一般流畅顺滑,不给用户带来卡顿体验。否则,就像内力不足的武林高手,招式施展起来也会大打折扣。

6. 尾声:3D 图表开启数据可视化新纪元

从最初的 2D 图表到如今的 3D 图表,SwiftUI 7 的 Charts 框架在数据可视化的江湖中不断进化,为开发者们提供了越来越强大的工具。

3D 图表的出现,不仅让数据可视化变得更加生动、直观,也为武林高手们开辟了一片全新的江湖。

在这里,宝子们可以凭借自己的智慧和技艺,运用 3D 图表这一绝世神功,将数据的魅力展现得淋漓尽致,为用户带来前所未有的数据探索体验。

在这里插入图片描述

在未来的 iOS 开发江湖中,3D 图表必将成为开发者们手中的一把利器,助力他们在数据可视化领域中披荆斩棘,创造出更多令人惊叹的应用。而每一位开发者,都将在这个充满机遇与挑战的江湖中,书写属于自己的传奇故事。让我们怀揣着对技术的热爱和追求,勇敢地踏入这片新江湖,探索 3D 图表的无限未来吧!

此时,数据的江湖,已经不再是一个简单的平面,而是充满了三维空间的无限元宇宙,正如你们已然成为了这片江湖中举世无双的大侠一样,棒棒哒!

那么,感谢各位少侠的观赏!再会啦!8-)

Flutter与iOS混合开发交互

1、安装Flutter环境

1、下载SDK并安装

docs.flutter.cn/get-started…

2、 配置环境

如果 ~/.zshenv 文件存在,请在文本编辑器中打开 Zsh 环境变量文件 ~/.zshenv。如果不存在,请创建 ~/.zshenv
export PATH=$HOME/development/flutter/bin:$PATH加入到文件的最后面

创建Flutter项目

以Flutter为主

以Flutter为主:意思是直接创建完整的flutter项目,里面就已经包含了iOS、Android等工程。直接用即可

在需要的目录中 执行 flutter create aiflutter

配置

进入iOS文件夹

这里需要注意: 需要用到CocosPods将Flutter作为组件导入到项目,但是Flutter并没有直接生成Podfile文件。需要自己init一个

在进行 Podfile install时,会有警告。 如果想要去掉警告,需要按照以下方式修改。但是修改之后会运行不起来

image.png

正确的应该是选中Debug.xcconfig、Release.xcconfig

image.png

直接运行会报错:

Command PhaseScriptExecution failed with a nonzero exit code

这是由于Run Script的脚本找不到正确路径

需要修改 Podfile文件

具体如下:

source 'https://github.com/CocoaPods/Specs.git'

# Uncomment this line to define a global platform for your project

platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

在使用过程中, 因为iOS工程是其他人创建后给我的,在进行pod install的时候,出现了路径找不到的报错: 修改这个路径

image.png

此时如果直接运行,将会报错

Unable to load contents of file list: '/Target Support Files/Pods-Runner/Pods-Runner-frameworks-Debug-input-files.xcfilelist'

需要回到Flutter项目目录下,执行flutter run.
其实在执行flutter create flutterdemo完成的时候,就已经提示了

In order to run your application, type:
  $ cd test
  $ flutter run
Your application code is in test/lib/main.dart.

报错

如果报错: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

Build Settings中搜索ENABLE_USER_SCRIPT_SANDBOXING 将其设置为NO,如果原本是NO,设置为YES后运行一次后再改为NO

错误处理
1、确保执行过flutter run
2、在Build Settings中搜索ENABLE_USER_SCRIPT_SANDBOXING 将其设置为NO
3、使用pod init新建一个podfile文件并修改里面的内容
4、PROJECT -> info -> Configurations中的Debug、Release 设置为对应的 Debug.xcconfig、Release.xcconfig
5、确认ios/Flutter路径下的Generated.xcconfig中的配置FLUTTER_ROOT、FLUTTER_APPLICATION_PATH是否正常

总结

1、安装FlutterSDK并配置其环境

2、使用命令创建Flutter项目flutter create flutterdemo

3、执行cd flutterdemo 和 flutter run命令

4、导入podfile文件并执行pod install命令

以iOS为主

以iOS为主意思是:手动创建一个iOS工程,将Flutter作为一个组件导入到iOS项目中

1、创建一个iOS工程AIIOSDemo,并进行pod
2、在同级目录下新建Flutter项目:flutter create -t module my_flutter\

image.png 3、在podfile中引入flutter

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '15.0'


# 1、在文件顶部添加 flutter_application_path
flutter_application_path = '../my_flutter'     #这里是刚才创建的flutter module名称
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')


target 'iOSDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  pod 'SnapKit'
  
  pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']
    
  
  // 2、引入路径
  install_all_flutter_pods(flutter_application_path)
  
end


# 3、添加这个 post_install 块
post_install do |installer|
  flutter_post_install(installer)
end

页面跳转

从原生跳转到Flutter页面 可参考链接

在使用Flutter之前,需要先注册GeneratedPluginRegistrant

//在AppDelegate中定义全局的flutterEngine
 lazy var flutterEngine: FlutterEngine = FlutterEngine(name: "com.brainco.gameEngine")


 private func initEngine() {
     // 在用到Flutter之前,要先注册这个方法
     //这个要在跳转方法之前运行环境,也可以在appdelegate里面启动就初始化,环境运行需要时间,单写在跳转方法里面靠前位置是不可以的。
     flutterEngine.run();
     GeneratedPluginRegistrant.register(with: flutterEngine);
 }
 
  • 直接以FlutterViewController为页面 在原生页面初始化按钮,并添加点击事件,在事件中实现以下代码:
   
    func jumpToFlutterPage() -> Void {
        
        let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
        
        /*
        // 可以通过MethodChannel传递参数
        let channel = FlutterMethodChannel(
            name: "com.example.app/flutter",
            binaryMessenger: flutterViewController.binaryMessenger
        )
        
        // 可选 -- 设置初始路由或传递参数
        channel.invokeMethod("initialRoute", arguments: "/targetPage")
         
        // 可选 -- 设置监听,执行Flutter调用原生的方法
        channel.setMethodCallHandler{[weak self] (call, result) in
            guard let strongSelf = self else { return }
            print("flutter 给到我 method:\(call.method) arguments:\(String(describing: call.arguments))")
        }
         */
        self.navigationController?.pushViewController(flutterViewController, animated: true)
    }
  • 将Flutter作为ChildViewController加入原生的viewController

class FlutterCustomViewController: BaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        
        let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine
        let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
        
        /*
        // 可以通过MethodChannel传递参数
        let channel = FlutterMethodChannel(
            name: "com.example.app/flutter",
            binaryMessenger: flutterViewController.binaryMessenger
        )
        
        // 可选 -- 设置初始路由或传递参数
        channel.invokeMethod("initialRoute", arguments: "/targetPage")
         
        // 可选 -- 设置监听,执行Flutter调用原生的方法
        channel.setMethodCallHandler{[weak self] (call, result) in
            guard let strongSelf = self else { return }
            print("flutter 给到我 method:\(call.method) arguments:\(String(describing: call.arguments))")
        }
         */
        
        self.addChild(flutterViewController)
        self.view.addSubview(flutterViewController.view)
        flutterViewController.view.snp.makeConstraints{make in
            make.left.right.top.bottom.equalToSuperview()
        }
    }

iOS与Flutter交互

Flutter 与原生存在三种交互方式 可参考链接

三种 Channel 之间互相独立,各有用途,但它们在设计上却非常相近。每种 Channel 均有三个重要成员变量:

  • name: 【重要参数】String类型,代表 Channel 的名字,也是其唯一标识符需要和Fluter中的定义保持一致
  • messager:【重要参数】BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具
  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器

MethodChannel

一般用于传递方法调用(method invocation)通常用于Flutter调用原生中某个方法

举例:使用场景-Flutter需要获取原生生成的用户UUID,并传递UUID做存储操作


// 引入Flutter
import Flutter

@objc class AppDelegate: FlutterAppDelegate {

// 枚举的方式定义方法名
enum FlutterMethodType: String {
    case saveUUID       = "saveUUID"        ///< 保存 UUID
    case getUUID        = "getUUID"         ///< 获取 UUID
}


    let controller = window?.rootViewController as! FlutterViewController

// 初始化参数,并设置回调handle

     func MethodChannelRegist(controller: FlutterViewController) {
        let methodChannel_channer = FlutterMethodChannel(
            name: "com.example/ai/snowflake",
            binaryMessenger: controller.binaryMessenger
        )
        methodChannel_channer.setMethodCallHandler { [weak self] (call, result) in
            guard let self = self else { return }
            self.flutterMethodChanner_channer(call: call, result: result)
        }
    }
    
    // flutter 调用 swift
    private func flutterMethodChanner_channer(call: FlutterMethodCall, result: FlutterResult) -> Void {
        if call.method == FlutterMethodType.getUUID.rawValue {
            let uuid = "uuid"
            result(uuid)
        }else if call.method == FlutterMethodType.saveUUID.rawValue {
            let success = true
            result(success)
        } else {
            result(FlutterMethodNotImplemented)
        }
    }
    
}

image.png

BasicMessageChannel

它是可以双端通信的,Flutter 端可以给 iOS 发送消息,iOS 也可以给 Flutter 发送消息。

// 全局,方便随时可以发送消息
 var basicMessageChannel: FlutterBasicMessageChannel? = nil
 
 // 其他和MethodChannel基本一致
 func BasicMessageChannelRegist(controller: FlutterViewController) {
        basicMessageChannel = FlutterBasicMessageChannel(name: "com.example/ai/snowflake",
                                                             binaryMessenger: controller.binaryMessenger)
        
        basicMessageChannel?.setMessageHandler { [weak self] (call, result) in
            guard let self = self else { return }
            self.flutterMethodChanner_channer(call: call as! FlutterMethodCall, result: result)
        }
        
        // 相比MethodChannel 最重要的区别就是这个 可以主动向Flutter发送消息
        basicMessageChannel?.sendMessage(["name":"隔壁老王","age":25])
        
    }
    
    // flutter 调用 swift
    private func flutterMethodChanner_channer(call: FlutterMethodCall, result: FlutterResult) -> Void {
        if call.method == "methodOne" {
            
        }else if call.method == "methodTwo" {
            
        } else {
            result(FlutterMethodNotImplemented)
        }
    }
 

image.png

EventChannel

只能是原生发送消息给 Flutter 端,例如监听手机电量变化,网络变化,传感器等。

func eventChannelRegist(controller: FlutterViewController) {
        let eventChannel = FlutterEventChannel(
          name: "com.example.demo/event",
          binaryMessenger: controller.binaryMessenger
        )
        eventChannel.setStreamHandler(self)
    }
    
    // MARK: FlutterStreamHandler
    var eventSink: FlutterEventSink? = nil
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = events
        return nil
    }
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        self.eventSink = nil
        return nil
    }
    
    func sendEvent(data: Any) {
        eventSink?(data) // 主动发送数据到 Flutter
      }

image.png

记录一次Flutter项目上传App Store Connect出现“Validation failed”错误的问题

描述

  • Flutter老项目,在flutter3.7之前的版本创建的
  • 现在用Flutter 3.16.9版本适配后,运行iOS都很正常
  • Xcode上 Product -> Archive 打包成功
  • 上传到App Store Connect,在validate环节报错,如下:

企业微信截图_b6551138-9586-4b55-aca0-420e99c9e670.png 错误日志:

...
Invalid Bundle. The bundle Runner.app/Frameworks/App.framework does not support the minimum OS Version specified in the Info.plist.
...

分析

现阶段App Store Connect 要求上传的ipa包的支持的最低系统版本要高于iOS 12.0,应该是项目里的配置或者一些第三方库的支持版本小于12.0了。需要从项目本身和第三方库两个方面着手处理。

处理

  1. 项目支持版本号设置:项目/ios/Podfile文件上面设置 platform :ios, '12.0' image.png

  2. 调整第三方库的支持版本号,同样是修改 项目/ios/Podfile文件 image.png

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
    end
  end
end

3.修改AppFrameworkInfo.plist文件里的MinimumOSVersion的值,一些老项目MinimumOSVersion的值都小于12.0(本次是有11.0调整为12.0)。 image.png

GoogleAdsOnDeviceConversion 库的作用与用法

GoogleAdsOnDeviceConversion 库详细报告

1. 概述

GoogleAdsOnDeviceConversion 是一个专为 iOS 应用设计的软件开发工具包(SDK),用于实现设备端转换测量(On-Device Conversion Measurement)。它的核心功能是帮助开发者在用户设备上直接收集和处理广告转换数据(如应用安装、重新安装等),以支持隐私保护的广告效果归因。该库特别适用于无法直接集成 Google Analytics for Firebase (GA4F) 的场景,或者需要独立 SDK 的情况。

1.1 主要作用

  • 隐私保护:通过在设备端处理转换数据,减少敏感数据传输,符合严格的隐私法规(如 GDPR)。
  • 独立性:提供独立于 Firebase 的解决方案,适合多样化的开发需求。
  • 广告效果优化:帮助广告主了解 iOS 应用广告的效果,优化营销策略。

1.2 适用场景

  • iOS 应用开发者希望在保护用户隐私的同时,测量广告转换效果。
  • 无法或不愿使用 Google Analytics for Firebase 的项目。
  • 需要与第三方归因平台(如 Adjust)集成的场景。

2. 集成方式

GoogleAdsOnDeviceConversion 支持通过 CocoaPods 和 Swift Package Manager 集成。以下是详细步骤:

2.1 使用 CocoaPods

  1. 打开项目的 Podfile,添加以下行:
    pod 'GoogleAdsOnDeviceConversion'
    
  2. 在终端中运行以下命令:
    pod install --repo-update
    

2.2 使用 Swift Package Manager

  1. 在 Xcode 中,点击 File > Add Packages
  2. 在搜索栏输入以下 GitHub 仓库 URL:
    https://github.com/googleads/google-ads-on-device-conversion-ios-sdk
    
  3. 选择版本(建议选择 "Up to Next Major Version"),然后点击 Add Package
  4. Xcode 将自动解析并下载依赖项。

2.3 注意事项

  • 如果您的应用使用 Firebase Analytics 11.14.0 或更高版本,GoogleAdsOnDeviceConversion 可能已自动包含,无需手动添加。
  • 确保您的 Xcode 版本为 16.0 或更高,目标 iOS 版本为 12.0 或更高。

3. 使用方法

以下是如何在 Swift 中使用 GoogleAdsOnDeviceConversion 的详细步骤。

3.1 Swift 中的使用

在 Swift 项目中,您需要执行以下步骤:

3.1.1 导入库

在需要使用该库的 Swift 文件顶部添加:

import GoogleAdsOnDeviceConversion
3.1.2 设置首次启动时间

在应用启动时(例如,在 AppDelegateSceneDelegateapplication(_:didFinishLaunchingWithOptions:) 方法中),设置应用的首次启动时间:

ConversionManager.sharedInstance.setFirstLaunchTime(Date())

此步骤对于准确的转换归因至关重要。

3.1.3 获取转换信息

使用以下方法获取聚合的转换信息,通常用于应用安装(.installation):

ConversionManager.sharedInstance.fetchAggregateConversionInfo(for: .installation) { aggregateConversionInfo, error in
    if let info = aggregateConversionInfo {
        // 使用 aggregateConversionInfo,例如作为 odm_info 参数
        print("Aggregate Conversion Info: \(info)")
    } else if let error = error {
        print("Error fetching conversion info: \(error)")
    }
}
  • aggregateConversionInfo 是一个字符串(例如 "abcdEfadGdaf"),可作为 odm_info 查询参数传递给广告平台。

3.3 示例代码

以下是一个完整的 Swift 示例,展示如何在应用启动时设置和获取转换信息:

import UIKit
import GoogleAdsOnDeviceConversion

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 设置首次启动时间
        ConversionManager.sharedInstance.setFirstLaunchTime(Date())
        
        // 获取转换信息
        ConversionManager.sharedInstance.fetchAggregateConversionInfo(for: .installation) { aggregateConversionInfo, error in
            if let info = aggregateConversionInfo {
                print("Aggregate Conversion Info: \(info)")
                // 示例:将 info 传递给广告平台
                // let url = URL(string: "https://your-ad-platform.com?odm_info=\(info)")!
            } else if let error = error {
                print("Error: \(error.localizedDescription)")
            }
        }
        return true
    }
}

4. 与其他 SDK 的集成

4.1 与 Firebase Analytics

  • 如果您的应用使用 Firebase Analytics 11.14.0 或更高版本,GoogleAdsOnDeviceConversion 可能已自动包含,无需手动添加。
  • 若需手动集成,确保 Podfile 中未重复添加依赖。

4.2 与 Adjust SDK

如果您使用 Adjust SDK 进行归因,可以通过 Adjust 的 ODM 插件启用设备端转换测量:

  • Podfile 中添加:
    pod 'Adjust/AdjustGoogleOdm'
    pod 'GoogleAdsOnDeviceConversion', '2.0.0'
    
  • 在应用启动时尽早调用 Adjust SDK 的 initSdk 方法,例如:
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Adjust.initSdk(with: yourConfig)
        return true
    }
    
  • 如果需要延迟 SDK 初始化,可使用 Adjust 的 First Session Delay 功能。
  • Adjust SDK 会自动处理首次启动时间捕获,确保归因准确。

5. 注意事项

以下是使用 GoogleAdsOnDeviceConversion 时需要注意的关键点:

方面 细节
首次启动时间 确保 setFirstLaunchTime() 设置的日期是应用的实际首次启动时间,否则可能影响归因准确性。
地域限制 该功能在欧洲经济区(EEA)、英国和瑞士不可用(参考 Google Ads Help)。
Firebase 集成 使用 Firebase Analytics 11.14.0 或更高版本时,无需手动添加此库。
版本兼容性 Adjust SDK 已测试版本 2.0.0 的 GoogleAdsOnDeviceConversion,建议验证新版本的兼容性。
隐私合规性 确保您的应用披露如何使用 Google 服务处理数据(参考 Google Ads 隐私披露)。

6. 总结

GoogleAdsOnDeviceConversion 库是 iOS 应用开发者精确衡量 Google Ads 广告系列带来的应用安装和后续应用内操作的关键工具。其主要目标是在严格遵守用户隐私标准的前提下,增强广告系列的优化和报告能力 。  

从功能上看,它是一个“设备端转化衡量插件”,旨在与 Firebase Analytics SDK 和 Google 的应用归因合作伙伴 (AAP) SDK 协同工作 。这种集成表明它并非一个独立的广告 SDK,而是一个专门的实用工具,用于在更广泛的 Google Ads 和 Firebase 生态系统中提高转化数据的精确度。  

该库在设计上持续强调“隐私保护” ,并特别提及“去标识化的临时信号” 。这表明 Google 正在对不断变化的隐私法规(如通用数据保护条例 (GDPR))和平台级变化(如 Apple 的应用跟踪透明度 (ATT) 框架)做出战略性和前瞻性的调整。通过在用户设备上直接进行归因,而无需将可识别的个人数据传输到设备之外,Google 旨在提供强大而有效的衡量能力,这些能力本身就符合现代隐私期望。这种方法将该库定位为在隐私受限环境中保持广告有效性的必要适应。

SwiftUI 新手必读:如何用纯 SwiftUI 在应用中实现分段控制?

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

前言


在现代应用程序开发中,分段控制(Segmented Control)是一种常用的界面元素,它由多个水平排列的部分组成,每个部分都可以作为一个互斥的按钮供用户选择。

在 SwiftUI 中,虽然没有专门的视图来实现分段控制,但苹果将其视为 Picker 的一种变体,这种设计理念其实是非常合理的,因为它们在功能上具有相似性。

接下来就来看看如何在 SwiftUI 中创建分段控制。

分段控制的概念

在 UIKit 中,我们熟悉的 UISegmentedControl 是专门用于实现这种控件的,而在 SwiftUI 中,Picker 则是其等效的实现方式。

Picker 是 SwiftUI 中用于选择互斥值的一种控件,与 UIKit 的 UIPickerView 类似。通过使用 Picker,我们可以轻松地创建分段控制。

如何创建分段控制

既然分段控制是 Picker 的一种变体,我们可以通过创建一个 Picker 视图,并通过 pickerStyle 修饰符应用 SegmentedPickerStyle() 来实现。以下是一个简单的示例:

struct ContentView: View {
    @Stateprivatevar selectedColorIndex = 0
    privatelet colors = ["红色""绿色""蓝色"]
    var body: some View {
        VStack {
            Picker("选择颜色", selection: $selectedColorIndex, content: {
                ForEach(0..<colors.count) { index in
                    Text(colors[index]).tag(index)
                }
            })
            .pickerStyle(SegmentedPickerStyle()) // <1>
            Text("选中的颜色 : \(colors[selectedColorIndex])")
        }
    }
}

在这个示例中,我们定义了一个 Picker,用于选择用户喜欢的颜色,并通过 SegmentedPickerStyle() 使其显示为分段控制的样式。

看下效果:

image.png

使用场景与建议

虽然分段控制和 Picker 在功能上相似,但它们在使用场景上有一些区别。

分段控制通常用于在不同的视图之间切换,例如在地图应用中,用户可以通过分段控制在地图、公交和卫星视图之间切换。而 Picker 则更适合用于从一长串选项中进行选择。

结论

SwiftUI 通过将分段控制视为 Picker 的一种变体,使得开发者能够以一种简洁而直观的方式实现这一功能。通过合理使用分段控制,我们可以为用户提供更友好的界面交互体验。

希望这篇文章能够帮助你更好地理解和使用 SwiftUI 中的分段控制。如果你有任何问题或建议,请在评论区告诉我们!

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

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

❌