阅读视图

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

SwiftUI 新容器视图 API 深度解析:轻松构建自定义布局

前言

自 SwiftUI 的第一个版本发布以来,它就拥有了几种容器视图。最常用的有 HStack、VStack、List 等。今年,Apple 引入了新的 API,使我们能够以全新的方式构建自定义容器视图。本周,我们将学习 SwiftUI 新的分解 API 的优势。

容器视图

容器视图就是一个可以包含其他视图的视图。我们可以使用 @ViewBuilder 闭包轻松定义一个容器视图。以下是一个示例:

struct Card<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack {
            content
        }
        .padding()
        .background(Material.regular, in: .rect(cornerRadius: 8))
        .shadow(radius: 4)
    }
}

如上面的例子所示,我们创建了 Card 视图,它是一个用于容纳任何 SwiftUI 视图的容器视图。它使用 @ViewBuilder 闭包包裹了内容,并添加了一个圆角背景和阴影。

struct ContentView: View {
    var body: some View {
        Card {
            Text("Hello, World!")
            Text("My name is Majid Jabrayilov")
        }
    }
}

这个 Card 类型使用起来非常简单。你只需创建一个 Card,并使用闭包提供内容。通过在 Card 容器视图内嵌入不同的视图,你可以在应用的多个屏幕中复用它。

这是使用容器视图的主要优势之一:你可以通过将共享的功能封装在容器视图中,在应用的不同地方重复使用它们。

想了解更多关于 @ViewBuilder 闭包的内容,可以查看我关于 “SwiftUI 中 @ViewBuilder 的强大功能” 的文章。

使用 ViewBuilder

@ViewBuilder 闭包让我们可以轻松地组合多个视图,并将一个视图嵌入到另一个视图中。但是如何从 @ViewBuilder 闭包中提取子视图呢?SwiftUI 引入了新的 API,允许我们重新组合视图。例如,我们可以从通过 @ViewBuilder 闭包构建的内容视图中提取子视图,并根据需要将它们放置。

struct Carousel<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(subviews: content) { subview in
                    subview
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .contentMargins(16)
    }
}

如上面的示例所示,我们使用了带有 subviews 参数的 ForEach 视图,这使我们能够提取内容视图的子视图并对它们进行迭代。

struct ContentView: View {
    var body: some View {
        Carousel {
            Color.yellow
            Color.orange
            Color.red
            Color.blue
            Color.green
        }
    }
}

SwiftUI 使用特定的 Subview 类型来公开提取视图的实例。它符合 View 协议,因此我们仍然可以附加额外的 SwiftUI 视图修饰符。它还为我们提供了 id 属性,这是一个唯一标识符,以及与特定视图关联的容器值。我们将在接下来的文章中更多讨论容器值。

访问子视图

另一种新的 API 允许我们通过索引访问子视图,而不是使用 ForEach 视图进行迭代。

struct Magazine<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView {
            Group(subviews: content) { subviews in
                if !subviews.isEmpty {
                    subviews[0]
                        .padding(.horizontal)
                        .containerRelativeFrame(.vertical) { length, _ in
                            return length / 3
                        }
                }
                
                if subviews.count > 1 {
                    ScrollView(.horizontal) {
                        LazyHStack {
                            ForEach(subviews[1...], id: \.id) { subview in
                                subview
                                    .containerRelativeFrame([.horizontal, .vertical])
                            }
                        }
                        .scrollTargetLayout()
                    }
                    .scrollTargetBehavior(.viewAligned)
                    .contentMargins(16)
                }
            }
        }
    }
}

在上面的示例中,我们使用了带有 subviews 参数的 Group 视图,它允许我们将子视图提取到一个名为 SubviewsCollection 的集合类型中。SubviewsCollection 类型符合 RandomAccessCollection 协议,并为我们提供了通过索引访问的功能。

组合子视图

如你所见,我们使用 Group 视图来分解内容视图,然后以另一种方式组合子视图。我们还利用了 id 参数的功能,允许我们使用 ForEach 视图与普通数据一起工作。

struct ContentView: View {
    var body: some View {
        Magazine {
            Color.yellow
            Color.orange
            Color.red
            Color.blue
            Color.green
        }
    }
}

可运行的 Demo

根据文章内容,我将提供一个可以展示如何使用 SwiftUI 新的容器视图 API 构建自定义视图的简单示例,包含 CardCarouselMagazine 容器视图。

import SwiftUI

// 定义 Card 视图,作为一个基本的容器视图
struct Card<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack {
            content
        }
        .padding()
        .background(Material.regular, in: RoundedRectangle(cornerRadius: 8))
        .shadow(radius: 4)
    }
}

// 定义 Carousel 视图,横向滚动的自定义容器视图
struct Carousel<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(subviews: content) { subview in
                    subview
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .contentMargins(16)
    }
}

// 定义 Magazine 视图,具有垂直和水平组合布局的自定义容器视图
struct Magazine<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView {
            Group(subviews: content) { subviews in
                // 第一个子视图为大图
                if !subviews.isEmpty {
                    subviews[0]
                        .padding(.horizontal)
                        .containerRelativeFrame(.vertical) { length, _ in
                            return length / 3
                        }
                }
                
                // 其余子视图为横向滚动小图
                if subviews.count > 1 {
                    ScrollView(.horizontal) {
                        LazyHStack {
                            ForEach(subviews[1...], id: \.id) { subview in
                                subview
                                    .containerRelativeFrame([.horizontal, .vertical])
                            }
                        }
                        .scrollTargetLayout()
                    }
                    .scrollTargetBehavior(.viewAligned)
                    .contentMargins(16)
                }
            }
        }
    }
}

// 主视图,使用自定义容器视图
struct ContentView: View {
    var body: some View {
        VStack {
            // 使用 Card 视图
            Card {
                Text("SwiftUI 容器视图示例")
                    .font(.headline)
                Text("使用 Card 容器轻松复用视图")
            }
            .padding()
            
            // 使用 Carousel 视图
            Carousel {
                Color.yellow
                Color.orange
                Color.red
                Color.blue
                Color.green
            }
            .frame(height: 100)
            .padding()
            
            // 使用 Magazine 视图
            Magazine {
                Color.pink
                Color.purple
                Color.teal
                Color.mint
            }
            .frame(height: 300)
        }
        .padding()
    }
}

// 主应用入口
@main
struct ContainerViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

功能概述

  1. Card:一个简单的容器视图,可以包裹任何内容并添加背景和阴影。你可以在应用中的多个地方使用该容器来保持一致的样式。
  2. Carousel:一个横向滚动的容器视图,可以自动排列并展示内容,适合展示横向滑动的图像或视图。
  3. Magazine:一个自定义的容器视图,允许你将第一个子视图设置为大图,其他子视图横向排列展示。类似于杂志布局。

运行这个Demo

此代码展示了如何在 SwiftUI 中构建自定义的容器视图,灵活地将不同的布局封装在容器中,以便在应用中多次复用这些布局模式。

总结

通过使用 SwiftUI 新引入的 API 以及容器视图,你可以轻松构建具有良好复用性的自定义布局,提升应用的开发效率和代码可维护性。

苹果、华为“撞档”上新 | Swift 周报 issue 62

前言

本期是 Swift 编辑组自主整理周报的第六十二期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。

Swift 周报在 GitHub 开源,欢迎提交 issue,投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。

人生两条路,一条在心中,唤作梦想,一条于脚下,叫做现实。Swift社区不扰繁华,不论悲欢,点头于心,踏步向前!👊👊👊

周报精选

新闻和社区:苹果、华为“撞档”上新引海外关注

提案:成员导入可见性提案正在审查。

Swift 论坛:讨论并行计算 DAG / 共享未来

推荐博文:在 Swift 中引入对 Oblivious HTTP 的支持

话题讨论:

有人说智能手机的霸主地位要换主了,你觉得的呢?

上期话题结果

如果真的不支持微信,会不会出现另外一个拥有类似功能的 App?

新闻和社区

苹果、华为“撞档”上新引海外关注

2024 年 9 月 13 日

本周,苹果公司、华为公司相继举行新品发布会,推出各自最新创新产品。中美两家高科技企业“撞档”上新,备受科技界及国际舆论关注,中国科技企业的创新能力引热议。 苹果公司推出了 iPhone 16 系列智能手机和苹果手表等新品。华为公司发布了全球首款三折叠屏手机,在铰链系统、屏幕弯折等方面实现多项技术突破。一些媒体在报道中对两家公司的发展状况及新产品进行对比。

美国“石英”财经网站撰文称,华为自去年 8 月发布 Mate 60 Pro 系列智能手机以来,在中国市场的表现优于苹果。今年 4 月,华为发布的报告称其利润连续第四个季度实现增长,这凸显了华为在美方制裁和打压下展示出的韧性。

“反观苹果,其 iPhone 销量同期下降了 19%,这是自 2020 年新冠疫情以来苹果在中国市场表现最糟糕的一次。今年一季度,苹果在中国智能手机市场的份额从 19.7% 跌至 15.7%。面对华为和其他中国本土智能手机制造商的激烈竞争,苹果不得不下调部分机型在中国市场的价格。”报道称。 美国消费者新闻与商业频道(CNBC)报道称,华为在 2019 年美国对中国科技企业实施制裁打压后遭受重创,如今在智能手机领域强势回归。华为及其他中国智能手机企业已在销售折叠屏手机,而苹果尚未进军该领域。

知名技术市场分析公司卡纳利斯咨询公司研究经理安伯·刘对媒体表示,华为和苹果新品发布时间相近,标志着中国高端市场新一轮竞争浪潮的开始。关键竞争领域将包括高端产品、软件功能和人工智能部署。华为的快速复苏“直接挑战到”苹果在中国市场的表现。中国是苹果的全球第二大市场,占其全球出货量的 20% 以上。

卡纳利斯咨询公司发布的数据显示,今年二季度,苹果在中国市场的出货量被挤出前五,排名退至第六。前五位首次被中国国内手机品牌包揽。

美国网络公司美国在线(AOL)以“苹果公司中国竞争对手华为抢了 iPhone 16 风头”为题撰文称,每年秋季苹果发布新款 iPhone 时,都是智能手机领域的绝对霸主。然而今年,其在中国最大的竞争对手之一华为在努力“改写剧本”。 美国有线电视新闻网报道称,美国此前的限制措施当时让华为的智能手机发展受到沉重打击,但如今华为再次“重返巅峰”,同时还在进军新业务。去年华为推出了一款与特斯拉 Model S 竞争的电动车。华为在人工智能发展方面也有远大抱负。

两家中美企业发布的最新产品也引来一些媒体和业内人士的评测。印度新闻网站“今日商业”撰文称,苹果的 iPhone 16 系列虽然有一些改进,但与上一代相比并没有引入任何重大变化。相比之下,华为的新款折叠屏手机提供了突破性的设计和许多高科技功能。华为一直高度重视折叠屏手机市场,这次发布的新产品进一步巩固了其地位。

路透社报道称,苹果最新发布的 iPhone 16 未能让投资者兴奋,因为大家期待已久的新产品中的人工智能功能仍处于测试模式。而华为推出业界首款三折叠屏手机,在争夺全球智能手机市场主导地位的斗争中继续加码。

科技新闻网站 technology.org 撰文指出,折叠屏手机的兴起反映了消费者对智能手机屏显更加灵活、外形更加新颖有更高的期待。华为推出的三折叠屏手机 Mate XT 等设备展示了功能和美学的融合,这是智能手机设计的未来发展方向。同时,人工智能技术的深度融合也是未来智能手机技术革新的一大重要趋势。Mate XT 体现的领先人工智能技术不仅能提升用户体验,同时能变革使用者与人工智能技术的互动。(来源:新华社)

苹果公司发布新品

2024 年 9 月 10 日

image.png

9月9日,在美国加利福尼亚州丘珀蒂诺市举行的苹果新品发布会后,人们体验新品。 当日,苹果公司举行新品发布会,推出iPhone 16系列智能手机和苹果手表等新品。(来源:光明网)

image.png

现已推出针对自动续期订阅的赢回优惠

2024 年 9 月 10 日

现在,你可以在 App Store Connect 中配置回头客优惠,这是一种针对自动续期订阅的新优惠。借助回头客优惠,你将能触达之前的订阅者,鼓励他们重新订阅你的 App 或游戏。例如,你可以创建提前支付优惠,对于标准续订价格为每年 39.99 美元的订阅项目,前六个月享受 9.99 美元的优惠价。Apple 会根据你的优惠配置,在不同位置向符合条件的顾客显示此类优惠,这些位置包括:

App Store 上的多个位置,包括你的产品页面,以及“Today”、“游戏”和 “App”标签页上的个性化推荐和编辑精选。 你 App 或游戏内的适当位置。

你通过自己的营销渠道分享的直接链接。

“订阅”设置。

在 App Store Connect 中创建回头客优惠时,你需要确定顾客资格,选择地区提供情况,并选取折扣类型。从今年秋季开始, 回头客优惠将向符合条件的顾客显示。

提案

通过的提案

SE-0443 精确控制编译器警告的标志 提案通过审查。该提案已在 第六十一期周报 正在审查的提案模块做了详细介绍。

正在审查的提案

SE-0444 成员导入可见性 提案正在审查。

在 Swift 中,有一些规则决定了是否会将另一个模块中的声明名称视为当前作用域的一部分。例如,如果你使用了 swift-algorithms 包,并且想要使用全局函数 chain(),那么你必须在引用该函数的文件中写上 import Algorithms,否则编译器会认为它超出了作用域:

// 缺少 'import Algorithms'
let chained = chain([1], [2]) // 错误:找不到 'chain' 的作用域

不过,对于成员声明(例如在结构体中声明的方法),其可见性规则却有所不同。当解析成员声明时,即使引入该成员的模块只是通过传递方式导入,成员也会处于作用域内。传递导入的模块可以是在另一个源文件中直接导入的模块,也可以是程序某个直接依赖项的依赖。这种不一致性可以理解为一个微妙的漏洞,而不是有意的设计决策,在很多 Swift 代码中它可能不会引起注意。

然而,当涉及到扩展的成员时,导入规则变得更令人惊讶,因为扩展和其名义类型(nominal type)可以在不同的模块中声明。

该提案通过更改规则,统一了名称查找的行为,使顶级声明和成员使用相同的标准进入作用域。

SE-0445 改进 String.Index 的打印描述 提案正在审查。

此提案符合 String.IndexCustomStringConvertible

Swift论坛

  1. 讨论SwiftIfConfig 库正在取代编译器的 #if 处理

内容大概:

Swift 编译器正在经历一项重大更新,新的 SwiftIfConfig 库将取代编译器中对 #if 指令的处理。这个库是 swift-syntax 包的一部分,目前已完成多个关键改进:

  1. 配置区域的实现:为 IDE 中的 #if 折叠功能提供支持,增强代码覆盖率分析。
  2. 替换 C++ 解析器中的 #if 条件逻辑:使大部分旧的 ParseIfConfig.cpp 代码不再需要,优化了对 #if 条件的解析。
  3. 在 ASTGen 中支持 #if 指令:新解析器通过支持 #if,提升了处理复杂语法结构的能力。
  4. 基于 #if 条件输出语法错误:根据 #if 指令的配置情况来决定哪些语法错误需要打印。
  5. 删除遗留的“内联文本”提取逻辑:进一步简化了编译器中的代码。

接下来的工作是从 C++ 语义 AST 中彻底移除 IfConfigDecl,这一改变不仅能使编译器代码更简洁,还能提高 #if 在不同语法规则中的扩展性。此外,这次更新还会保留一些重要的编译器行为,例如在不活跃的 #if 块中抑制变量未使用的警告,以及抑制 try 和 throw 相关的警告。

在 SourceKit 方面,多个查询已被 swift-syntax 和 swift-format 工具取代,更新会废弃旧的查询并在下一个 Swift 版本中完全移除这些处理#if的查询。与此类似,Swift 编译器前端的 swift-indent 模式也将被移除,因其功能有限,且已有更现代的工具(如 swift-format)可供使用。

总体而言,这次更新将 Swift 编译器中的 #if 处理逻辑迁移到 swift-syntax 库中,极大简化了主编译器的代码基础,标志着一大进步。

  1. 提议重新审视允许更多非标识符字符的反引号分隔标识符

内容大概:

本文提出再次允许在反引号(backticks)中使用包含空格和其他非标识符字符的标识符。过去曾提出过类似的提案(SE-0275),但被拒绝。本文试图基于新的信息和使用经验重新审视该提案。

  1. 描述性测试命名:

之前的拒绝理由之一是希望测试框架能够提供不同的方法来为测试用例附加字符串。虽然新的 swift-testing 框架已经实现了这一点,但当前的方法要求用户为测试命名两次,这不仅冗余,还引入了不一致。例如:

@Test("tapping pushes the nav stack")
func tappingPushesTheNavStack() {
    // 测试代码...
}

这种方法导致测试报告和测试 UI 中使用描述性名称,但调试器、回溯以及代码导航工具仍使用函数声明名。为了避免这种不一致,本文建议允许函数名直接使用描述性名称,例如:

@Test func `tapping pushes the nav stack`() {
    // 测试代码...
}
  1. 模块命名:

在大型代码库中,模块命名是一个挑战。当前的做法是将模块构建目标路径转换为有效的标识符名,例如:

import my_cool_project_ui_navigation

但这种做法增加了自动化工具的复杂性,特别是在处理依赖关系和导入管理时。本文建议允许直接使用包含非标识符字符的路径作为模块名,例如:

import `my/cool/project/ui/navigation`

这将简化导入语句并减少自动化工具的复杂性。

  1. 其他注意事项:

提案还讨论了与工具链的边缘情况和潜在的未来方向。例如,建议通过定义一个字符集来限制反引号中的允许字符,以增强对未来 Unicode 扩展的适应性。

总结:该提案旨在简化代码中的测试命名和模块命名,同时减少不必要的复杂性,并提高代码的可读性和一致性。

  1. 讨论如果没有办法拦截“fatalError”,则会对服务器造成危害

内容大概:

在服务器端使用Swift时,fatalError 无法被拦截是有害的。虽然通过将功能隔离到Docker容器中,并自动重启失败的容器,可以在一定程度上缓解问题,但在生产环境中,代码无论多么理想化,都会因各种原因导致崩溃。Swift早期编译器版本中的无效代码、缺失库调用导致的致命错误、内存泄漏等问题,都可能导致容器崩溃。

有两种应对严重错误的思路:一种是类似Erlang的“快速失败”方法,失败时生成新的轻量级进程再次执行代码;另一种是更常见的方法,假设大多数异常不会破坏内存,程序可以继续运行。fatalError() 的设计假设这些错误不符合程序模型,并且假设大多数代码在隔离的进程中运行。这种方法是否适用于服务器端的Swift,尤其是在使用结构化并发时,仍然有待验证。

总的来说,Swift需要根据不同的应用场景来调整对严重错误的处理策略,特别是在服务器环境下。

  1. 讨论并行计算 DAG/共享未来?

内容大概:

本文讨论了如何在 Swift 中并行化计算任务。具体任务可以描述为以下函数:

func compute(_ input: [Key]) -> [Key: Value]

该函数的结果包含所有输入中的键,还会生成一些在计算过程中发现的额外键。存在一个依赖有向无环图(DAG),描述所有值之间的依赖关系。

通过一个简单的例子展示了计算的形式,例如计算整数n的阶乘并将其映射到字典中:

func compute(_ input: [Int]) -> [Int: Int] {
    var r: [Int: Int] = [:]

    func fact(_ x: Int) -> Int {
        if let y = r[x] { return y; }
        let y = x == 0 ? 1 : x * fact(x - 1)
        r[x] = y
        return y
    }

    for z in input { _ = fact(z) }
    return r
}

在这个讨论中提出了一种可能的并行化方案:在最终的for循环中为每个输入元素启动并行任务。同时,考虑到DAG的结构,一个键的值的计算可能依赖于另一个键的值,如果该键的值已经在另一个线程中开始计算,那么该计算可能会暂停等待。这种情况类似于共享Future系统。

此外,作者希望在每个线程中批量处理计算结果,并且只在批量足够大时将结果合并到最终结果中,以减少同步的开销。同时,允许某些键值对被重复计算,以避免使用共享的Future系统。

本次的讨论希望通过讨论找到合适的并行计算解决方案。

  1. 讨论关于“间接枚举”语义的澄清

内容大概:

最近作者在工作项目中建议将一些枚举标记为 indirect,以减少它们占用的栈内存。然而,在解释 indirect 实际上做了什么时,我发现很难清楚地说明,除了引用编译器内部机制之外。

官方文档中关于 indirect enum 的唯一提及是“递归枚举”,这是最常见的使用场景。然而,还有其他有效的使用场景,论坛上也经常讨论这些,但从官方文档来看,这是否是预期用途并不明确。

作者认为值得更明确地记录 indirect 在底层实际做了什么,并提到它在内存优化场景中的作用。虽然语言本身从未“官方”保证某些内容的内存分配方式,但了解其在不同场景下的行为会有所帮助。

例如,一个常见的场景是链表类型使用 indirect enum 实现。编译器可能足够智能,可以证明某个链表在函数内不逃逸,从而将堆分配转化为动态栈分配。

另一个例子是全局常量的值是 indirect enum,目前生成的代码会执行堆分配,但理论上可以将这些间接情况放入静态空间。

此外,由于 indirect 的情况是不可变的,因此不能对这些盒子的引用标识做出任何保证,除非使用不安全的技巧来观察其标识。

推荐博文

在 Swift 中引入对 Oblivious HTTP 的支持

摘要: 这篇官方文章介绍了 Swift 中对 Oblivious HTTP(OHTTP)的支持,并发布了新的 SwiftNIO 包 SwiftNIO Oblivious HTTP。Oblivious HTTP 通过加密 HTTP 请求并结合第三方中继服务,保护客户端的身份信息,增强隐私性,避免暴露诸如 IP 地址等数据。

SwiftNIO Oblivious HTTP 包提供两个主要库:

ObliviousHTTP:实现了 RFC 9292 中定义的二进制 HTTP 编码方案和 Oblivious HTTP。 ObliviousX:提供加密功能的 API,支持 Oblivious HTTP 及其他数据的加密。 文章通过代码示例演示了如何使用这些库进行 HTTP 消息的序列化、反序列化和加密解密。还提到了未来的开发计划,包括与 SwiftNIO 更好的集成、对其他 Swift 类型的支持,以及对分块 OHTTP 的支持。

SwiftNIO Oblivious HTTP 仍处于早期开发阶段,期待社区的反馈和贡献。。

Swift 中的任务和任务组

摘要: 这篇博客介绍了 Swift 中的任务(Task)和任务组(Task Group),并讲解了它们的使用方法及相关高级技巧。

任务(Task) 是 Swift 并发编程的一部分,允许在非并发环境中创建并发任务,任务在创建后立即运行。文章展示了如何创建任务、处理任务的错误和取消任务。Swift 提供了 Task.checkCancellation() 主动抛出错误终止任务,或通过 Task.isCancelled 检查任务是否被取消。还介绍了如何设置任务优先级和任务的生命周期状态(运行中、暂停、取消和完成)。

任务组(Task Group) 用于组合并发执行的多个任务,并等待所有任务完成后返回结果。通过 withTaskGroupwithThrowingTaskGroup 可以创建任务组,并发执行任务。文章提供了如何处理任务组中的错误、如何避免数据竞争,并展示了取消任务组的用法。

最后,作者强调了避免并发修改任务组的操作,推荐使用 cancelAll() 来取消任务组中的所有子任务,以及 addTaskUnlessCancelled() 来确保任务组未被取消时才添加新任务。。

递归枚举在 Swift 中的妙用

摘要: 这篇博客介绍了 Swift 中递归枚举的使用及其优势。递归枚举允许枚举的某些情况包含自身实例,适用于建模层次化或递归结构的数据,如文件系统。通过 indirect 关键字,Swift 可以安全地处理递归引用,避免内存问题。

文章首先展示了如何用递归枚举实现文件系统模型,并引入了文件、文件夹和别名的概念。然后通过代码示例,展示了如何使用递归枚举创建嵌套文件结构,并递归计算文件夹中的总项目数。

此外,文章还解释了在引用自身时如何正确使用 indirect 关键字,并指出当引用通过集合类型(如数组)实现时,不需要 indirect 标记。

话题讨论

近期苹果公司推出了iPhone 16系列智能手机和苹果手表等新品。华为公司发布了全球首款三折叠屏手机,在铰链系统、屏幕弯折等方面实现多项技术突破,展示了功能和美学的融合。有人说智能手机的霸主地位要换主了,你觉得的呢?

1.华为也不过是一个高个子小兵罢了,想比肩苹果,可笑可笑。 2.苹果手机近年来创新科技不足,被华为超越是迟早的事。 3.于消费者而言,霸主更替并无坏处,品牌产商有足够的压力和动力才能创造更好的产品。

关于我们

Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。

特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。

Apple 新品发布会亮点有哪些 | Swift 周报 issue 61

前言

本期是 Swift 编辑组自主整理周报的第六十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。

Swift 周报在 GitHub 开源,欢迎提交 issue,投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。

其实根本就没有什么假如,每个人的人生都不会重新设计。Swift社区也有梦想,也懂现实,生气不如争气,改变胜过抱怨。

周报精选

新闻和社区:苹果 9 月 10 日举行今年最重磅新品发布会,iPhone 16 亮相?都有哪些看点?

提案:精确控制编译器警告的标志的提案正在审查中

Swift 论坛:提议讨论整数泛型参数

推荐博文:Swift 中的 Typed Throws

话题讨论:

如果微信不支持 iPhone 16,你选微信还是 Apple

上期话题结果

根据投票结果可以看出,超过一半的朋友希望 Apple 能更好地将 AI 与 Swift 和 Xcode 集成。期待 iPhone 16 的发布带来亮眼的新功能。

新闻和社区

苹果据称正洽谈投资 OpenAI 英伟达也有意跟投

2024 年 8 月 30 日

据媒体援引消息人士报道,两大科技巨头苹果公司和英伟达均有意投资人工智能(AI)研究公司 OpenAI。

周三有消息称,OpenAI 正在洽谈新一轮融资,计划以超过 1000 亿美元的估值筹集数十亿美元资金,风投公司兴盛资本(Thrive Capital)将领投此轮融资,投资达到 10 亿美元。此外,作为 OpenAI 最大股东,微软也将参与这轮融资。

知情人士称,苹果正就投资 OpenAI 进行谈判,英伟达也已讨论过加入对 OpenAI 的最新融资。据悉,英伟达商谈在 OpenAI 新一轮融资中投入 1 亿美元。

据悉,OpenAI 首席财务官 Sarah Friar 周三一份备忘录中表示,该公司正寻求新的融资,但没有透露具体细节。Friar 称,OpenAI 将利用这笔融资获得更多的计算能力,并为其他运营费用提供资金。

目前尚不清楚苹果和微软计划投资多少金额。如果相关谈判取得成功,这意味着全球最有价值的三家科技公司都将成为 OpenAI 的股东。

OpenAI 成立之初是一家非营利组织,该公司在 2019 年转为一家营利性初创企业。自那以来,微软共向 OpenAI 投资了 130 亿美元,拥有后者 49% 的股份。

苹果在今年 6 月的全球开发者大会(WWDC)上宣布推出苹果智能(Apple Intelligence)系统,该公司还官宣了和 OpenAI 的合作关系。

苹果当时宣布,准备在今年晚些时候将 OpenAI 的聊天机器人 ChatGPT 集成到系统平台中,包括 iOS 18、iPadOS 18 和 macOS Sequoia,由 GPT-4o 提供支持。

苹果多年来大量投资了代工伙伴,部分原因是为了确保其设备零部件的供应,但该公司很少投资初创企业。

相比之下,作为 AI 龙头股的英伟达在投资领域更为活跃。自 2023 年以来,英伟达旗下投资部门 NVentures 已投资了十多家AI制药企业。(来源:科技板日报)

消息称苹果公司服务部门将裁员约百人,波及 Apple Books / Apple News 等业务

2024 年 8 月 28 日

北京时间8月28日上午,据彭博社援引知情人士消息称,苹果公司宣布在其服务部门裁员约 100 人。

当地时间周二,苹果公司通知了受影响的员工,这些员工分别在高级副总裁 Eddy Cue 的服务部门的几个不同团队工作。此次裁员包括一些工程师职位,受到影响最多的是负责苹果 Apple Books 图书应用和苹果书店的团队。而与此同时,其他服务团队也在裁员,包括运营 Apple News 的团队。

报道称,Apple Books 已不再是公司的重点工作,也不再被视为服务阵容的“重要组成部分”。据知情人士透露,随着时间的推移,Apple Books 应用程序仍有望获得新的功能,但 Apple News 的裁员不意味其“不再受到重视”。

裁员在苹果公司并不常见。据IT之家此前报道,今年 4 月,苹果向美国加利福尼亚州提交的必要文件显示公司永久裁员 600 多人。外界认为,可能是苹果放弃汽车项目之后,2000 多人的开发团队中部分转岗到苹果其它项目,而这 600 多人被苹果裁员。(来源:IT之家)

苹果 9 月 10 日举行今年最重磅新品发布会,iPhone 16 亮相?都有哪些看点?

2024 年 8 月 23 日

知名科技记者马克·古尔曼当地时间 8 月 23 日发文称,苹果公司计划在 9 月 10 日举行今年最大的产品发布会,届时该公司将推出最新款 iPhone、Watch 和 AirPods。

古尔曼援引知情人士消息表示,虽然具体时间尚未公布,但该公司正在为这一天做准备。发布会之后,新款手机预计将于 9 月 20 日正式上市销售,这与苹果近年来的做法一致。

马克·古尔曼此前曾表示,有知情人士向他透露,苹果的洛杉矶新店正在为 9 月 16 日那一周开业全力准备。众所周知,苹果公司喜欢在新产品上市之际开新店。

按照以往规律,苹果在 9 月 10 日开完发布会后,要等到周五( 9 月 13 日)开启预订,那么一周后新 iPhone 正式上市的日子( 9 月 20 日)正好赶上洛杉矶新店开业。

对于新一代iPhone,有业内人士称,总体而言,iPhone 16 和 iPhone 15 不会有太大不同。上月苹果在 iPhone 15 的 Pro 以上机型“限量”上线了个人智能化系统 Apple Intelligence,iPhone 16 也将拥有这一先进人工智能(AI)技术加持的工具。此外,iPhone 16 的 Pro 机型屏幕会更大,会拥有新的照相功能,例如用于拍照的专用按钮。

iPhone 16 ‌机型将搭载配合 Apple Intelligence 使用的更强大芯片 A18,整个 iPhone 16 ‌系列都将支持新的 iOS 18‌。

iPhone 16 Pro 和 Pro Max 的显示屏会稍微大一些,尺寸分别为 6.3 英寸和 6.9 英寸。

整个 iPhone 16‌ 系列机型都将有操作按钮(Action Button),这将是基本款的 iPhone 首次采用该按钮。

整个 iPhone 16 ‌系列机型都将新增一个拍照按钮 Capture Button,位于右侧电源键下方。该按钮类似于数码相机上的快门按钮,轻按该按钮可自动对焦,让拍摄更精准,重按该按钮可直接完成拍照,用户在该按钮左右滑动可缩放画面,该按钮可切换拍照和录影功能。

iPhone 16 Pro 和 Pro Max 将配备 4800 万像素的超广角摄像头,像素较 iPhone 5 的 1200 万大幅提升。

相比 iPhone,苹果智能手表和耳机的变化可能更大。媒体称,Apple Watch Series 10 将比前代更薄,但屏幕更大。苹果将更新中低端的 AirPods 产品线,将首次在中端 AirPods 上提供降噪功能,将自 2019 年以来首次更新入门级的 Airpods。

对于苹果公司来说,这次产品发布非常关键,因为最近几个季度苹果的智能手机和可穿戴设备销量一直不太理想。

如果新的 iPhone 16 在 9 月 20 日上市,这意味着该设备的部分收入将计入第四财季财报,苹果预计这一财季营收将同比增长 5%。

而更大的销售份额则需要在下一个财季才能实现,分析师预测,在那个时期,恰逢假日季节,苹果营收将大幅增长 7%,达到 1284 亿美元。

在今年 6 月的年度开发者大会上,苹果公布了一项期待已久的人工智能战略,即将其最新人工智能技术整合到一系列应用程序中,并将 OpenAI 的聊天机器人 ChatGPT 引入其设备。

可穿戴设备也将迎来一些重大变化。据悉,Apple Watch Series 10 系列的型号将会更薄,但会配备更大的屏幕。预计新手表的起售价为 399 美元,并提供两种尺寸选择。

苹果还在更新其 AirPods 产品线,预计将推出新的中低端版本。据悉,这是苹果首次在中端 AirPods 上提供主动降噪 (ANC) 功能,而入门级型号则将进行 2019 年以来的首次更新。

该公司还计划今年晚些时候开始将其 Mac 电脑过渡到 M4 处理器,但 Mac 的更新通常要到 iPhone 发布会后一个月左右。

提案

正在审查的提案

SE-0443 精确控制编译器警告的标志 提案正在审查。

本提案引入了新的编译器选项,允许对编译器如何发出特定警告进行精细控制,使它们可以作为警告或错误来处理。

Swift论坛

  1. 提议整数泛型参数

内容大概

这篇讨论围绕通过引入整数泛型参数来增强 Swift 处理具有内联存储的固定容量数据结构的能力。对于像嵌入式 Swift 这样注重性能的代码库,这特别有用,在这些场景中,开发人员需要高效且类型安全的数据结构。目前,Swift 的局限性需要繁琐且容易出错的技术,例如手动创建具有特定元素数量的结构体,并使用不安全的操作进行索引。

动机:

这个提议的动机源于 Swift 缺乏对使用内联存储的固定大小或固定容量集合的原生支持。这些集合对于编译时容量固定的场景非常重要,例如固定大小的数组、具有固定桶数的哈希表或具有特定维度的矩阵。通过允许在泛型中使用整数参数,相同的实现可以在不同大小之间重用,从而提高代码的可重用性并确保更强的类型安全性。

提议的解决方案:

该提案引入了通过整数参数对泛型类型进行参数化的概念。这是通过在泛型参数列表中使用新的语法实现的,其中整数通过 let 关键字声明。例如:

struct Vector<let N: Int, T> { /* 实现待定 */ }

在这里,N 是一个字面整数参数,允许使用固定大小 N 定义 Vector 类型。这使得可以创建固定维度的矩阵或向量,例如:

struct Matrix4x4 {
    var matrix: Vector<4, Vector<4, Double>>
}

详细设计:

  1. 声明语法:
    • 整数泛型参数使用 let 关键字后跟参数名称和类型 Int 声明。
    • 例如: struct Matrix<let N: Int, let M: Int> { ... }
  2. 实例化:
    • 这些类型可以使用字面整数值实例化,也可以通过引用周围泛型上下文中的整数参数来实例化。
  3. 约束和算术:
    • 提案允许在整数参数之间添加约束,例如确保两个参数相等或某个参数等于特定值。
    • 未来的方向包括支持在这些泛型参数中进行算术运算,从而实现更复杂的关系,如组合向量或矩阵。
  4. 类型兼容性:
    • 该设计通过扩展现有的泛型语法保持源代码兼容性。为值参数引入 let 确保了清晰性,并防止在将来引入不同类型的值参数时产生歧义。
  5. ABI 兼容性:
    • 该提案不会影响现有 Swift 代码的 ABI,因为它引入了新功能,而不是改变现有行为。

未来方向:

该提案是 Swift 中迈向更高级功能的基础步骤:

  1. 固定大小和固定容量集合:
    • 未来的提案可能会引入充分利用此功能的标准库类型,例如固定大小的数组或缓冲区。
  2. 泛型参数中的算术:
    • 能够使用整数参数进行算术运算,例如将两个数组的维度相加以创建一个新数组。
  3. 非整数值泛型参数:
    • 扩展该功能以允许其他类型的值参数,可能会在类型级别支持更丰富的约束和操作。
  4. 参数包:
    • 未来的工作可能还会探索整数参数的可变参数包,这可能允许定义具有任意维度的多维数组或矩阵。
  1. 讨论[GSoC-2024] Visual Studio Code 中 Swift 宏的扩展(详细文章) - Lokesh.T.R(Alex Hoppen 和 Adam Fowler)

内容大概

Lokesh T.R 是来自印度 Chennai Vel Tech 大学的二年级学生,他参与了 2024 年的 Google Summer of Code (GSoC) 项目,专注于在 Visual Studio Code (VS Code) 中扩展 Swift 宏。在导师 Alex Hoppen 和 Adam Fowler 的指导下,Lokesh 的主要目标是实现一个代码操作,允许用户在 VS Code 编辑器中直接查看 Swift 宏的展开内容。

项目概述

该项目旨在通过引入一个新功能来增强 VS Code 中的 Swift 开发体验,即通过调用“Expand Macro”代码操作,将 Swift 宏展开并在“peeked”编辑器窗口中显示生成的内容。该功能将包含在与 Swift 6.1 捆绑的 SourceKit-LSP 中,并将在下一个 VS Code Swift 扩展版本中发布。

主要成就

  1. 引入新 LSP 扩展:
    • workspace/peekDocuments: 这个 LSP 扩展允许 SourceKit-LSP 服务器在 VS Code 中的一个 peek 窗口中显示存储在特定位置的内容。
    • workspace/getReferenceDocument: 这个扩展使得 VS Code Swift 扩展可以向 SourceKit-LSP 服务器请求并检索文档(例如 Swift 宏展开)的内容。
  2. 自定义 URL 方案:
    • Lokesh 引入了一个新的自定义 URL 方案 (sourcekit-lsp://),用于编码生成任何形式内容(特别是 Swift 宏展开)所需的数据。引入的第一个文档类型是 swift-macro-expansion。
  3. 在 Peeked 编辑器中的宏展开:
    • 当用户调用“Expand Macro”代码操作时,系统会使用 SourceKitD 为宏展开生成一个参考文档 URL,然后发出 workspace/peekDocuments 请求。这些内容随后将在 VS Code 的 peeked 编辑器中显示。
  4. 语义功能和嵌套宏展开:
    • Lokesh 扩展了项目范围,支持在宏展开预览中进行语义功能(如跳转到定义、悬停时的快速帮助和语法高亮显示)。这是通过重新使用源文件的构建参数来欺骗 SourceKitD,使其为参考文档提供这些功能。
    • 该系统还支持嵌套宏展开,其中在生成的宏展开中存在的宏本身也可以被展开。自定义参考文档 URL 通过允许每个宏展开引用其父级,从而促进了这种嵌套,使嵌套展开得以高效和无缝地进行。
  5. 支持其他基于 LSP 的编辑器:
    • 虽然新 LSP 扩展最初是为 VS Code 设计的,但 Lokesh 致力于为其他基于 LSP 的编辑器(如 Neovim)提供基本的宏展开支持,使用标准的 LSP 请求。这使得这些编辑器能够显示宏展开,尽管形式更为简单,方法是将展开内容存储在临时文件中然后显示出来。

实施细节

实施过程中涉及到几个关键组件的紧密互动:

  • VS Code-Swift 扩展: 作为 VS Code 和 SourceKit-LSP 之间的桥梁。
  • SourceKit-LSP: 提供必要的编辑器功能,并通过 LSP 进行通信。
  • SourceKitD: 嵌入在 Swift 编译器中的后台服务,提供宏展开所需的原始数据和操作。

为了实现项目目标,Lokesh 和他的导师引入了上述的自定义 LSP 扩展和 URL 方案,并确保这些功能平滑地集成到现有的 Swift 工具链和 VS Code 扩展中,注重用户体验和性能。

未来方向

  1. 测试用例和文档:
    • Lokesh 计划实施涵盖各种嵌套宏级别所有语义功能的全面测试用例。这包括在 VS Code 中的端到端测试,以确保“Expand Macro”代码操作在真实世界场景中的稳健
  1. 提议Non-Discardable throwing Tasks

内容大概

在 Swift 中,使用包含抛出函数的任务时,常见的问题是错误可能会被无意中忽略。这是因为当前的 Task 初始化器默认允许丢弃错误,这可能导致严重且难以发现的 bug。

当前行为:

当前的 Task 初始化器定义如下:

extension Task where Failure == any Error {
    @discardableResult
    public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> Success)
}

由于使用了 @discardableResult 属性,开发者可以忽略错误而不会收到任何警告,这可能导致关键错误被无意中忽略。

提议的更改:

提议中建议移除 Task 初始化器中的 @discardableResult 属性。这样一来,忽略错误将不再是默认行为,从而提高代码的安全性,确保开发者在必要时有意识地选择忽略错误。

例如,要有意忽略错误,开发者需要显式地写成:

_ = Task {
    try await dontCareIfThisThrowsOrNot()
}

这一更改旨在引入警告,帮助发现潜在的 bug,提高代码的安全性,并减少无意中的错误处理遗漏。

论据:

作者指出,当前默认允许丢弃错误的行为在原始的 Swift 结构化并发提案中并没有得到充分的理由支持。鉴于它带来的问题,重新审视这一设计决策可能会提升代码的可靠性。

一个相关的讨论主题也指出了同样的问题,这表明 Swift 社区中的其他开发者也遇到了类似的挑战。

  1. 讨论最小化结构体的 MemoryLayout 大小/步幅有哪些好处?

内容大概

在 Swift 中,结构体的内存布局由其存储属性的声明顺序决定。通过重新排列这些属性,可以减少结构体的 MemoryLayout.size 和 MemoryLayout.stride。这种优化看似有吸引力,但它的实际好处是什么呢?

主要好处:

  1. 存在类型优化:
  • Swift 可以优化存在类型(Any 或 any P),如果类型的大小为 3 个机器字或更小(在 64 位机器上为 24 字节),则可以将值内联存储在存在类型中。通过优化结构体的布局以符合这一限制,可以避免在频繁使用这些类型时进行不必要的内存分配。
  1. “大型”类型的阈值:
  • 如果类型超过 5 个机器字(在 64 位机器上为 40 字节),Swift 会将其视为“大型”并以不同方式传递,通常通过堆栈分配并传递指针。虽然这本身并不坏,但当复制该值时,这会导致增加的内存操作,从而影响性能。
  1. 汇编级别的考虑:
  • 一些开发者希望使用更少的寄存器来表示类型(例如,2 个寄存器而不是 4 个),以实现代码大小和性能上的微小提升。然而,对于大多数开发者来说,这些问题微不足道,不值得过多关注。
  1. 讨论并发 101

内容大概

  1. 挂起点和线程切换

在 Swift 中,当使用 async/await 处理异步函数时,挂起点是关键。这些是你代码中的特定点,由 await 标记,函数可以在这些点暂停或“挂起”。在挂起期间,执行此函数的线程可以切换到其他任务。这一过程是 Swift 并发能力的一部分。

如果你的代码没有这些挂起点(即没有 await 调用),函数将从头到尾执行而不暂停。在这种情况下,线程将被“阻塞”在当前任务上,无法进行其他工作。重要的是,Swift 不会引入超出 await 显式定义的附加挂起点。这一设计确保了开发者可以依赖同步函数的可预测行为,并清楚地区分同步和异步函数。

  1. 并发 vs. 并行
  • 并发 指的是任务可以独立进展的能力。然而,这并不一定意味着这些任务在同一时间运行。并发允许任务的执行交错进行,即多个任务正在进行中,但它们可能不会同时执行。
  • 并行 则是任务实际在同一时间运行,通常是在不同线程上。当系统有多个线程可用时,可以实现任务的同时执行。

使用 async/await 时,并不能保证并行执行。例如,如果你在一个任务中有多个 await 调用(如 await a; await b; await c),它们将在该任务中按顺序执行,而不是并行执行。并行执行只能在有多个任务在不同线程上运行时发生。

  1. 执行交错

交错是一种允许任务共享单线程时间的技术。在像 Swift 的并发模型这样的协作多任务环境中,这一点非常重要。当一个任务到达 await 点并挂起时,它会让出线程,允许其他任务运行。这种交错使得即使在单线程环境中,任务也能被视为并发执行,尽管它们并未并行执行。

因此,并发是并行的前提条件,但它们并不相同。一个系统可以有并发任务,但这些任务可能不会并行执行,取决于运行时如何调度它们。

  1. 函数执行和误解

一个常见的误解是“并发函数”的概念。实际上,函数本身并不并发;而是任务的执行可以是并发的。单个线程一次只能执行一段代码。这意味着虽然多个任务可以同时进行,但在给定线程上一次只能执行一个任务。

async/await 的好处在于它简化了异步代码的结构,使其比传统的回调或续传样式更易于阅读和推理。它还通过允许任务暂停和让出控制来实现更好的资源管理,而不是不必要地阻塞线程。

总之,async/await 在 Swift 中通过挂起点、执行交错和明确区分同步与异步函数来支持高效的并发。虽然它支持并行,但这取决于多个线程的可用性和运行时的调度决策。

推荐博文

SwiftUI 中 View 之间的通信 【macOS App】

摘要: 这篇博客探讨了 SwiftUI 中视图之间的通信方式,并与前端框架如 Vue 和 React 进行对比。文章首先介绍了父视图传递消息给子视图的方式,类似于 Vue 的 props,但使用方法有所不同。接着,文章讲述了子视图如何向父视图传递消息,分别介绍了使用 @Binding、闭包(Closure)回调函数、以及共享 Observable 数据的方式。最后,文章讨论了在多层级视图间使用环境变量传递消息的方式,类似于 Vue 的 Inject/Provide 和 React 的 context。作者总结认为,尽管 SwiftUI 提供了多种视图通信方式,但其灵活性和易用性与前端框架相比仍有所不足,尤其是在复杂场景下依然需要结合使用 Combine。

Swift 协议的进化之路:深入理解不透明类型与装箱类型

摘要: 这篇博客详细探讨了 Swift 协议中 some 和 any 两种关键字的使用及其背后的类型系统。文章首先解释了为什么使用带有关联类型的协议时会出现编译错误,并介绍了三种解决方案:使用 any 关键字创建装箱类型(Boxed Type)、使用泛型、以及使用 some 关键字创建不透明类型(Opaque Type)。

文章深入分析了不透明类型的作用,强调 some 关键字在编译时确定类型,但对外隐藏具体实现,适合简化 API 复杂性并提升性能。同时,文章也讨论了装箱类型的特性,指出 any 关键字在运行时确定具体类型,虽然灵活但有一定的性能开销。

作者强调了在不同场景下选择 some 或 any 的重要性,指出 some 适合编译时能确定类型的场景,而 any 则适合需要运行时确定类型的情况。了解这两者的区别对编写高效、简洁的 Swift 代码至关重要。

Swift 中的 Typed Throws

摘要: 这篇博客介绍了 Swift 6.0 中的新功能“Typed Throws”。过去,Swift 的错误抛出机制无法显式指定可能抛出的错误类型,开发者需要手动检查函数实现以了解错误类型。Swift 6.0 引入了“typed throws”,允许开发者在定义函数时明确指定抛出的错误类型,使代码更具可预测性和类型安全性。这一特性不强制要求使用,可以与旧的错误抛出方式兼容。博客还讨论了向后兼容性,解释了 Swift 编译器如何将未指定错误类型的抛出函数转换为使用 any Error 类型。通过这项改进,开发者能够更清晰地处理特定的错误类型,提高代码的可读性和安全性。

话题讨论

如果微信不支持 iPhone 16,你选微信还是 Apple

1、微信 2、Apple

关于我们

Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。

特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。

提升代码调试技巧:从思维到实践

摘要

调试是软件开发中的关键部分,它不仅帮助开发者找到代码中的错误,还能提高代码质量和开发效率。本文将从调试工具的使用、错误信息的解读、问题定位以及如何培养高效的调试思维等方面,系统地介绍提升调试技巧的方法,并通过实际案例展示调试过程中的思路和步骤。

引言

在开发过程中,每一位程序员都会不可避免地遇到各种各样的错误和问题。对于初学者来说,调试往往会成为令人头疼的难题,而即使是经验丰富的开发者,也经常会陷入复杂的调试过程中。高效的调试技巧能显著提升问题解决的速度,并减少不必要的挫折感。本文旨在分享一些实用的调试技巧和方法,帮助开发者掌握调试过程中的思路和工具。

调试工具的使用

断点调试

断点调试是最基本的调试方式之一,它允许开发者在代码执行过程中暂时中止程序,并逐步检查变量、表达式和函数的状态。在大多数现代 IDE(如VSCode、PyCharm)中,都内置了断点调试功能。

实践示例代码

def add_numbers(a, b):
    return a + b

def main():
    x = 5
    y = 10
    result = add_numbers(x, y)  # 在这里设置断点
    print(f"The result is {result}")

if __name__ == "__main__":
    main()

在上述代码中,开发者可以通过在 result 赋值的地方设置断点,观察函数的输入和输出,判断是否得到了期望的值。

调试器的使用

调试器不仅允许设置断点,还可以逐步执行代码、查看栈帧、检查变量的值。下面以 Python 的 pdb 为例展示如何手动进行调试。

pdb 调试代码

import pdb

def divide_numbers(a, b):
    pdb.set_trace()  # 进入调试模式
    return a / b

print(divide_numbers(10, 0))

当程序执行到 pdb.set_trace() 时,控制台将进入调试模式,可以使用 n 逐行执行,使用 p 查看变量的值。这对于复杂的函数调试非常有用。

阅读和解读错误信息

分析错误栈

当程序抛出异常时,错误栈(stack trace)是开发者最好的朋友。它提供了错误的源头和发生的位置。理解栈信息能够快速帮助开发者找到问题所在。

代码示例

def divide(a, b):
    return a / b

print(divide(10, 0))

执行该代码会抛出 ZeroDivisionError,并显示错误栈。开发者可以通过分析栈信息,迅速定位错误源。

常见的调试技巧

二分法定位问题

对于大型代码项目,如果没有明确的错误提示,逐行排查显然非常耗时。此时,可以使用“二分法”调试:即将代码分成两部分,测试前半部分,若无错,则继续测试后半部分,直到找到问题所在。

日志调试

日志调试是另一种高效的调试方法。通过记录程序运行过程中的重要信息,开发者可以在不依赖IDE的情况下回溯问题发生时的状态。适当的日志级别(如 INFODEBUGERROR)可以帮助开发者了解程序的运行情况。

日志模块代码

import logging

logging.basicConfig(level=logging.DEBUG)

def multiply_numbers(a, b):
    logging.debug(f"multiply_numbers called with a={a}, b={b}")
    return a * b

result = multiply_numbers(5, 10)
logging.info(f"The result is {result}")

日志可以帮助开发者在无法使用断点或调试器的场景下,追踪程序执行流程并发现问题。

调试的思维方式

从错误中学习

调试不仅仅是发现和修复问题的过程,更是开发者提高自己编程能力的机会。每次错误的出现都是对代码逻辑的挑战,因此我们可以从中学习如何避免类似问题。

假设与验证

高效的调试思维是从假设开始的。在调试时,开发者首先应基于代码行为和日志,推测出问题可能的原因,然后通过修改代码或加入日志,验证自己的假设是否正确。这个过程可以帮助迅速缩小问题范围。

避免盲目猜测

调试的过程中,最忌讳的就是盲目猜测而没有系统性的测试。每次调试时,都应基于已有的线索做出推测,再通过验证来逐步排除错误。

QA环节

问:什么情况下使用断点调试,什么情况下使用日志调试?

答:断点调试适用于调试小规模代码或问题容易复现的场景。而日志调试适用于大规模系统或无法实时附加调试器的场景,例如在生产环境下。

问:如何提升调试效率?

答:调试的关键在于培养一种严谨的思维方式。建议在编码过程中加入合理的错误处理和日志,使用二分法快速定位问题区域,同时逐步培养对错误栈信息的敏感度。

总结

提升调试技巧不仅仅是掌握工具的使用,还需要培养一种严谨的思维方式。通过合理使用断点、日志和调试器,结合假设与验证的思路,开发者可以大幅提升调试效率,快速解决问题。

未来的调试工具将更加智能化,可能引入 AI 技术帮助开发者自动定位问题和推荐解决方案。此外,分布式系统和微服务架构的复杂性日益增加,如何对复杂环境下的错误进行调试将成为新的挑战。掌握调试的基本技巧和思维,依然是面对未来开发挑战的基础能力。

参考资料

如何以编程方式解析 XCResult 包的内容

介绍

XCResult 包是一个包含运行一组测试结果详细信息的包或目录。这些包由 Xcode(或命令行中的 xcodebuild)生成,并提供了有关所运行测试的丰富信息,包括测试的名称、持续时间、状态以及它们生成的任何附件(如截图或日志)。

查找 XCResult 包

在 Xcode 中,你可以在测试运行后通过转到“报告导航器”并从列表中选择你感兴趣的包来查找和检查 XCResult 包:

分享 XCResult 包

如果你想与其他人分享该包,可以右键单击“报告导航器”中的包并选择“在 Finder 中显示”以打开包所在的目录。无论你是从命令行使用 xcodebuild 运行测试还是在 Xcode 中运行测试,所有 .xcresult 包都生成在应用的 Logs/Test 目录中的 Derived Data 中,你可以双击 .xcresult 文件在 Xcode 中打开并检查包的内容。

解析 XCResult 包

当你在 CI/CD 环境中运行应用的测试时,XCResult 包变得更加重要,因为没有它们,关于测试失败的唯一信息将是 xcodebuild 命令的日志。此外,对 CI/CD 机器的访问通常受到限制且繁琐,因此检索特定运行的 .xcresult 包并不总是那么简单。

这就是为什么通常最好让你选择的 CI/CD 服务在测试失败时将 XCResult 包作为工件上传到你的工作流程中,以便开发人员可以下载并检查结果。虽然这在开发者体验方面是一个重大改进,但反馈并不是即时的,因为需要开发人员下载包并在他们的机器上打开它。

自动解析 XCResult 包的内容

如果你能够以编程方式解析 XCResult 包的内容并提取所需信息,而无需打开 Xcode,那不是很好吗?这样,你可以自动化检查测试结果的过程,并为开发人员提供有关测试失败的即时反馈。这听起来很不错,但当你检查 .xcresult 包的内容时,你很快会发现内容不可读,这使得以编程方式解析它们的任务变得有些挑战性:

使用 XCResultKit 解析包的内容

幸运的是,对于我们来说,有一些工具可以在解析 XCResult 包的内容时使我们的生活变得更轻松。其中一个用 Swift 编写的库,我们将在本文中使用的是 David House 的 XCResultKit。

初始化库

首先,我们需要将库导入到我们的项目中作为 Swift Package。在这种情况下,我们将构建一个 Swift 可执行文件,该文件将使用 XCResultKit 从 .xcresult 包中提取信息:

Package.swift

// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "ResultAnalyzer",
    platforms: [
        .macOS(.v13)
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"),
        .package(url: "https://github.com/davidahouse/XCResultKit.git", exact: "1.2.0")
    ],
    targets: [
        .executableTarget(
            name: "ResultAnalyzer",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "XCResultKit", package: "XCResultKit")
            ]
        ),
    ]
)

在可执行文件的主文件中,我们现在可以导入库,要求提供 .xcresult 包的路径,并使用用户提供的路径初始化一个 XCResult 对象:

XCResultAnalyzer.swift

import ArgumentParser
import Foundation
import XCResultKit

@main
struct XCResultAnalyzer: ParsableCommand {
    @Argument(help: "The path to an `.xcresult` bundle")
    var bundle: String
    
    func run() throws {
        guard let url = URL(string: bundle) else { return }
        let result = XCResultFile(url: url)
    }
}

获取调用记录

读取包内容的第一步是获取信息记录。该记录包含所有元数据和信息,用于从包中检索其余数据:

XCResultAnalyzer.swift

func run() throws {
    guard let url = URL(string: bundle) else { return }
    let result = XCResultFile(url: url)

    guard let invocationRecord = result.getInvocationRecord() else { return }
}

信息记录包含有关测试运行的一些顶级信息,例如发生的操作、遇到的问题的详细摘要以及测试运行的指标:

XCResultAnalyzer.swift

func run() throws {
    guard let url = URL(string: bundle) else { return }
    let result = XCResultFile(url: url)

    guard let invocationRecord = result.getInvocationRecord() else { return }

    print("✅ Ran \(invocationRecord.metrics.testsCount ?? .zero) tests and skipped \(invocationRecord.metrics.testsSkippedCount ?? .zero)")
    print("❌ \(invocationRecord.issues.testFailureSummaries.count) test failures")
    print("🧐 Ran actions: \(invocationRecord.actions.compactMap { $0.testPlanName })")
}

使用我们之前检查的 .xcresult 包运行可执行文件,我们会得到以下输出:

✅ Ran 3 tests and skipped 0
❌ 1 test failures
🧐 Ran actions: ["AutomatedTesting"]

获取测试信息

获取给定测试的特定信息要复杂一些,因为你需要遍历包中的所有操作,获取测试计划信息,然后才能访问个别测试的特定信息。

让我们首先从包中检索所有失败的测试:

XCResultAnalyzer.swift

func run() throws {
    guard let url = URL(string: bundle) else { return }
    let result = XCResultFile(url: url)
    
    guard let invocationRecord = result.getInvocationRecord() else { return }
    
    // 1
    let testBundles = invocationRecord
        .actions
        .compactMap { action -> ActionTestPlanRunSummaries? in
            guard let id = action.actionResult.testsRef?.id, let summaries = result.getTestPlanRunSummaries(id: id) else {
                return nil
            }
            
            return summaries
        }
        .flatMap(\.summaries)
        .flatMap(\.testableSummaries)
    
    let allFailingTests = testBundles
        // 2
        .flatMap(\.tests)
        // 3
        .flatMap(\.subtests)
        .filter { $0.testStatus.lowercased() == "failure" }
}

让我们回顾一下包,并将其结构映射到代码中的注释:

导出屏幕录制

现在我们有了失败的测试,我们可以获取包含所有步骤的摘要,检索第一步的屏幕录制附件并导出它:

XCResultAnalyzer.swift

func run() throws {
    // ...
    let screenRecordings = allFailingTests
    .compactMap { test -> ActionTestSummary? in
        guard let id = test.summaryRef?.id else { return nil }
        
        return result.getActionTestSummary(id: id)
    }
    // 1
    .flatMap(\.activitySummaries)
    // 2
    .first?
    // 3
    .attachments
    .filter { $0.name == "kXCTAttachmentScreenRecording" && $0.uniformTypeIdentifier == "public.mpeg-4" } ?? []
        
    for screenRecording in screenRecordings {
        let tempFileDirectory = URL.temporaryDirectory
        result.exportAttachment(attachment: screenRecording, outputPath: tempFileDirectory.path())
    }
}

让我们再次查看包,并将其结构映射到代码中的注释。

可运行 Demo

上面详细介绍了理论逻辑。下面根据这个些功能提供一个可以运行的 Demo。

这个 Demo 将使用 XCResultKit 库来解析 XCResult 包的内容,并提取测试运行的基本信息和失败测试的屏幕录制。

初始化 Swift Package

首先,我们创建一个新的 Swift Package 项目。在终端中运行以下命令来创建项目:

swift package init --type executable
cd [YourProjectName]

然后编辑 Package.swift 文件以添加依赖项:

// swift-tools-version: 5.6

import PackageDescription

let package = Package(
    name: "XCResultParserDemo",
    platforms: [
        .macOS(.v12)
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"),
        .package(url: "https://github.com/davidahouse/XCResultKit.git", exact: "1.2.0")
    ],
    targets: [
        .executableTarget(
            name: "XCResultParserDemo",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "XCResultKit", package: "XCResultKit")
            ]
        ),
    ]
)

编写主文件

接下来,我们在 Sources/XCResultParserDemo/main.swift 中编写主文件代码。这个文件将导入库,处理命令行参数,并解析 XCResult 包的内容。

import ArgumentParser
import Foundation
import XCResultKit

@main
struct XCResultAnalyzer: ParsableCommand {
    @Argument(help: "The path to an `.xcresult` bundle")
    var bundle: String
    
    func run() throws {
        guard let url = URL(string: bundle) else {
            print("Invalid URL")
            return
        }
        let result = XCResultFile(url: url)

        // 获取调用记录
        guard let invocationRecord = result.getInvocationRecord() else {
            print("Could not retrieve invocation record")
            return
        }

        // 输出基本信息
        print("✅ Ran \(invocationRecord.metrics.testsCount ?? .zero) tests and skipped \(invocationRecord.metrics.testsSkippedCount ?? .zero)")
        print("❌ \(invocationRecord.issues.testFailureSummaries.count) test failures")
        print("🧐 Ran actions: \(invocationRecord.actions.compactMap { $0.testPlanName })")

        // 获取失败的测试
        let testBundles = invocationRecord
            .actions
            .compactMap { action -> ActionTestPlanRunSummaries? in
                guard let id = action.actionResult.testsRef?.id, let summaries = result.getTestPlanRunSummaries(id: id) else {
                    return nil
                }
                
                return summaries
            }
            .flatMap(\.summaries)
            .flatMap(\.testableSummaries)
        
        let allFailingTests = testBundles
            .flatMap(\.tests)
            .flatMap(\.subtests)
            .filter { $0.testStatus.lowercased() == "failure" }

        // 导出失败测试的屏幕录制
        let screenRecordings = allFailingTests
        .compactMap { test -> ActionTestSummary? in
            guard let id = test.summaryRef?.id else { return nil }
            
            return result.getActionTestSummary(id: id)
        }
        .flatMap(\.activitySummaries)
        .first?
        .attachments
        .filter { $0.name == "kXCTAttachmentScreenRecording" && $0.uniformTypeIdentifier == "public.mpeg-4" } ?? []
            
        for screenRecording in screenRecordings {
            let tempFileDirectory = URL.temporaryDirectory
            let outputPath = tempFileDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
            try result.exportAttachment(attachment: screenRecording, outputPath: outputPath.path)
            print("Screen recording exported to: \(outputPath.path)")
        }
    }
}

代码解释

  1. 导入库和定义命令:我们导入了 ArgumentParserFoundationXCResultKit 库,并定义了一个主结构体 XCResultAnalyzer,它遵循 ParsableCommand 协议以处理命令行参数。

  2. 处理命令行参数@Argument 属性包装器用于定义命令行参数。在这里,我们要求用户提供一个 .xcresult 包的路径。

  3. 解析 URL 和初始化 XCResultFile:我们将用户提供的路径转换为 URL 对象,并使用 XCResultFile 类初始化它。

  4. 获取调用记录:我们调用 getInvocationRecord() 方法来获取调用记录,这包含了测试运行的元数据和详细信息。

  5. 输出基本信息:我们输出了测试的总数、跳过的测试数量、失败的测试数量和执行的操作计划名称。

  6. 获取失败的测试:我们遍历调用记录中的操作,获取测试计划运行摘要,过滤出所有失败的测试。

  7. 导出屏幕录制:我们遍历失败测试的活动摘要,过滤出屏幕录制附件,并将它们导出到临时目录中。

运行 Demo

确保你的项目目录中有一个 .xcresult 包。然后,在终端中导航到项目目录并运行以下命令:

swift run XCResultParserDemo /path/to/your.xcresult

这将解析提供的 XCResult 包,并输出测试运行的基本信息和任何失败测试的屏幕录制路径。

通过这个 Demo,你可以以编程方式解析 XCResult 包的内容,并提取有用的信息以改进测试和 CI/CD 工作流。

结论

就是这样!下次运行可执行文件并提供 .xcresult 包的路径时,你将获得导出到临时目录的失败测试的屏幕录制,随时可以分享至任何需要的地方。

Swift 中的函数式核心与命令式外壳:单向数据流

timer_app_cover.png

前言

之前,我们讨论了在 Swift 中的函数式核心与命令式外壳的概念。其目标是通过值类型提取纯逻辑,并将副作用保持在薄薄的对象层中。本周,我们将展示如何以单向数据流的方式应用这一方法。

函数式核心

函数式核心是负责我们应用中所有逻辑的层,我们希望通过单元测试验证它们。它应该是纯粹的,没有任何副作用。我们希望提供输入并验证输出。通常,单向数据流的实现需要许多接收状态和动作并返回新状态的 reducer 函数。让我们在代码中定义 reducer 函数。

如果你不熟悉单向数据流的概念,我强烈建议你阅读我关于“在 SwiftUI 中类似 Redux 的状态容器”的系列文章。

typealias Reducer<State, Action> = (State, Action) -> State

正如你所见,reducer 函数接受当前状态和要应用于该状态的动作,并返回一个新状态。我正在开发一个间歇性禁食追踪的应用。让我们看看我如何实现计时器逻辑。

struct TimerState: Equatable {
    var start: Date?
    var end: Date?
    var goal: TimeInterval
}

enum TimerAction {
    case start
    case finish
    case reset
}

let timerReducer: Reducer<TimerState, TimerAction> = { state, action in
    var state = state

    switch action {
    case .start:
        state.start = .now
    case .finish:
        state.end = .now
    case .reset:
        state.start = nil
        state.end = nil
    }

    return state
}

这是我代码库中实现计时器管理逻辑的真实示例。正如你所见,它是纯粹的,没有任何副作用。它允许我快速使用单元测试验证逻辑,无需使用 mocks 和 stubs。

import XCTest

final class TimerReducerTests: XCTestCase {
    func testStart() {
        let state = TimerState(goal: 13 * 3600)
        XCTAssertNil(state.start)
        let newState = timerReducer(state, .start)
        XCTAssertNotNil(newState.start)
    }
}

像结构体和枚举这样的值类型是实现应用逻辑的极佳工具,既纯粹又非常可测试。但我们仍然需要副作用。例如,我想通过 CloudKit 与朋友分享计时器状态。

命令式外壳

命令式外壳是通过值类型表示应用状态的对象层。我们还利用对象层进行副作用操作,并将结果应用于状态之上。首先定义一个持有状态的通用对象。

@MainActor public final class Store<State, Action>: ObservableObject {
    @Published public private(set) var state: State

    private let reducer: Reducer<State, Action>

    public init(
        initialState state: State,
        reducer: @escaping Reducer<State, Action>
    ) {
        self.reducer = reducer
        self.state = state
    }

    public func send(_ action: Action) {
        state = reducer(state, action)
    }
}

这是使用 Store 类定义的命令式外壳。正如你所见,我们使用对象层持有通过值类型表示的应用状态。对象层允许我们分享应用状态,并使其成为单一事实来源。我们还通过利用 MainActor 并仅通过将动作传递给 Store 类型的 send 方法来允许变更,提供线程安全。这就是我们在函数式核心与命令式外壳的理念下实现单向数据流的方式。但我们仍然缺少副作用。

副作用

命令式外壳应为我们提供进行副作用操作的方法。我们应该将副作用与应用的纯逻辑分开,但我们仍希望通过集成测试来测试副作用。让我们引入一种称为 Middleware 的新类型,它定义了一个副作用处理程序。

typealias Middleware<State, Action, Dependencies> = (State, Action, Dependencies) async -> Action?

Middleware 类型的主要思想是拦截纯动作,进行副作用操作(如异步请求),并返回一个新的动作,我们可以将其传递给 store 并进行归约。让我们将此功能添加到 Store 类型中。

@MainActor public final class Store<State, Action, Dependencies>: ObservableObject {
    @Published public private(set) var state: State

    private let reducer: Reducer<State, Action>
    private let dependencies: Dependencies
    private let middlewares: [Middleware<State, Action, Dependencies>]

    public init(
        initialState state: State,
        reducer: @escaping Reducer<State, Action>,
        dependencies: Dependencies,
        middlewares: [Middleware<State, Action, Dependencies>] = []
    ) {
        self.reducer = reducer
        self.state = state
        self.dependencies = dependencies
        self.middlewares = middlewares
    }

    public func send(_ action: Action) async {
        state = reducer(state, action)

        await withTaskGroup(of: Optional<Action>.self) { [state, dependencies] group in
            for middleware in middlewares {
                group.addTask {
                    await middleware(state, action, dependencies)
                }
            }

            for await case let action? in group {
                await send(action)
            }
        }
    }
}

正如你所见,我们使用新的 Swift 并发特性在 Store 类型中实现异步工作。它允许我们并行运行副作用并将动作传递给 store。通过标记 Store 类型为 @MainActor,我们确保了对状态的访问。使用 TaskGroup,我们自动获得了副作用的协作取消。Store 类型还持有所有依赖项(如网络、通知中心等),以便提供给 middlewares。

struct TimerState: Equatable {
    var start: Date?
    var end: Date?
    var goal: TimeInterval
    var sharingStatus = SharingStatus.notShared
}

enum SharingStatus: Equatable {
    case shared
    case uploading
    case notShared
}

enum TimerAction: Equatable {
    case start
    case finish
    case reset
    case share
    case setSharingStatus(SharingStatus)
}

let timerReducer: Reducer<TimerState, TimerAction> = { state, action in
    var state = state

    switch action {
    case .start:
        state.start = .now
    case .finish:
        state.end = .now
    case .reset:
        state.start = nil
        state.end = nil
    case .share:
        state.sharingStatus = .uploading
    case let .setSharingStatus(status):
        state.sharingStatus = status
    }

    return state
}

struct TimerDependencies {
    let share: (Date, Date?) async throws -> Void
}

let timerMiddleware: Middleware<TimerState, TimerAction, TimerDependencies> = { state, action, dependencies in
    switch action {
    case .share:
        guard let start = state.start else {
            return .setSharingStatus(.notShared)
        }

        do {
            try await dependencies.share(start, state.end)
            return .setSharingStatus(.shared)
        } catch {
            return .setSharingStatus(.notShared)
        }
    default:
        return nil
    }
}

下面是实现 middleware 的示例代码。正如你所见,我们拦截传递给 store 的动作,进行异步请求,并向系统提供另一个动作。我们还可以通过 mock TimerDependencies 类型轻松编写集成测试。

import XCTest

final class TimerMiddlewareTests: XCTestCase {
    func testSharing() async throws {
        let state = TimerState(goal: 13 * 3600)
        let dependencies: TimerDependencies = .init { _, _ in }
        let action = await timerMiddleware(state, .share, dependencies)
        XCTAssertEqual(action, .setSharingStatus(.shared))
    }
}

想了解更多关于将异步闭包用作依赖项的信息,请查看我的“在 Swift 中的微应用架构:依赖注入”一文。

import SwiftUI

struct RootView: View {
    @StateObject var store = Store(
        initialState: TimerState(goal: 13 * 3600),
        reducer: timerReducer,
        dependencies: TimerDependencies.production,
        middlewares: [timerMiddleware]
    )

    var body: some View {
        NavigationView {
            VStack {
                if let start = store.state.start, store.state.end == nil {
                    Text(start, style: .timer)
                    
                    Button("Stop") {
                        Task { await store.send(.finish) }
                    }

                    Button("Reset") {
                        Task { await store.send(.reset) }
                    }
                } else {
                    Button("Start") {
                        Task { await store.send(.start) }
                }
            }
            .navigationTitle("Timer")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Share") {
                        Task {
                            await store.send(.share)
                        }
                    }
                }
            }
        }
    }
}

可运行 Demo

上面详细介绍了理论逻辑。下面根据这个些功能提供一个可以运行的 Demo。

我们将创建一个可以运行的 SwiftUI 应用示例,该应用将展示如何使用函数式核心与命令式外壳的理念来实现单向数据流和管理副作用。这个示例将实现一个简单的计时器应用,允许用户启动、停止、重置计时器并分享计时状态。

函数式核心部分

首先,我们定义应用的状态和动作,并实现一个 reducer 函数来管理状态变化。

import SwiftUI
import Combine

// 定义计时器状态
struct TimerState: Equatable {
    var start: Date?
    var end: Date?
    var goal: TimeInterval
    var sharingStatus = SharingStatus.notShared
}

// 定义计时器动作
enum TimerAction: Equatable {
    case start
    case finish
    case reset
    case share
    case setSharingStatus(SharingStatus)
}

// 定义共享状态
enum SharingStatus: Equatable {
    case shared
    case uploading
    case notShared
}

// 定义 Reducer 函数
typealias Reducer<State, Action> = (State, Action) -> State

let timerReducer: Reducer<TimerState, TimerAction> = { state, action in
    var state = state

    switch action {
    case .start:
        state.start = .now
    case .finish:
        state.end = .now
    case .reset:
        state.start = nil
        state.end = nil
    case .share:
        state.sharingStatus = .uploading
    case let .setSharingStatus(status):
        state.sharingStatus = status
    }

    return state
}

命令式外壳部分

接下来,我们定义一个 Store 类来持有应用的状态,并处理副作用。

@MainActor
public final class Store<State, Action, Dependencies>: ObservableObject {
    @Published public private(set) var state: State

    private let reducer: Reducer<State, Action>
    private let dependencies: Dependencies
    private let middlewares: [Middleware<State, Action, Dependencies>]

    public init(
        initialState state: State,
        reducer: @escaping Reducer<State, Action>,
        dependencies: Dependencies,
        middlewares: [Middleware<State, Action, Dependencies>] = []
    ) {
        self.reducer = reducer
        self.state = state
        self.dependencies = dependencies
        self.middlewares = middlewares
    }

    public func send(_ action: Action) async {
        state = reducer(state, action)

        await withTaskGroup(of: Optional<Action>.self) { [state, dependencies] group in
            for middleware in middlewares {
                group.addTask {
                    await middleware(state, action, dependencies)
                }
            }

            for await case let action? in group {
                await send(action)
            }
        }
    }
}

副作用处理

定义一个中间件来处理异步副作用,比如共享计时状态。

typealias Middleware<State, Action, Dependencies> = (State, Action, Dependencies) async -> Action?

struct TimerDependencies {
    let share: (Date, Date?) async throws -> Void
}

let timerMiddleware: Middleware<TimerState, TimerAction, TimerDependencies> = { state, action, dependencies in
    switch action {
    case .share:
        guard let start = state.start else {
            return .setSharingStatus(.notShared)
        }

        do {
            try await dependencies.share(start, state.end)
            return .setSharingStatus(.shared)
        } catch {
            return .setSharingStatus(.notShared)
        }
    default:
        return nil
    }
}

SwiftUI 界面

最后,我们创建一个 SwiftUI 界面来展示计时器功能,并连接到 Store

struct RootView: View {
    @StateObject var store = Store(
        initialState: TimerState(goal: 13 * 3600),
        reducer: timerReducer,
        dependencies: TimerDependencies(share: { start, end in
            // 模拟共享计时状态的逻辑
            print("Shared from \(start) to \(String(describing: end))")
        }),
        middlewares: [timerMiddleware]
    )

    var body: some View {
        NavigationView {
            VStack {
                if let start = store.state.start, store.state.end == nil {
                    Text(start, style: .timer)

                    Button("Stop") {
                        Task { await store.send(.finish) }
                    }

                    Button("Reset") {
                        Task { await store.send(.reset) }
                    }
                } else {
                    Button("Start") {
                        Task { await store.send(.start) }
                    }
                }
            }
            .navigationTitle("Timer")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Share") {
                        Task {
                            await store.send(.share)
                        }
                    }
                }
            }
        }
    }
}

@main
struct TimerApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}

代码运行截图

代码解释

  1. 状态和动作:我们定义了 TimerStateTimerAction 来表示计时器的状态和可执行的动作。
  2. Reducer 函数timerReducer 函数接受当前状态和动作,并返回一个新的状态。这个函数是纯函数,没有副作用,方便进行单元测试。
  3. Store 类Store 类持有应用的状态,并提供 send 方法来处理动作。我们使用 Swift 的并发特性来处理异步任务和副作用。
  4. 中间件timerMiddleware 用于处理异步副作用,比如共享计时状态。它拦截动作,执行异步任务,并返回一个新的动作来更新状态。
  5. SwiftUI 界面RootView 使用 Store 提供的状态和动作来构建界面。用户可以启动、停止、重置计时器,并共享计时状态。

这个示例展示了如何使用函数式核心与命令式外壳的理念来实现一个简单的计时器应用,利用 Swift 的最新特性处理异步任务和副作用。

总结

这篇文章讨论了如何在 Swift 中结合使用函数式核心与命令式外壳的理念来实现单向数据流,并详细展示了如何在代码中实现这些理念,包括使用 Swift 并发特性处理异步任务和管理副作用。通过这种架构,开发者可以在保持代码清晰和易于测试的同时,处理复杂的应用状态和副作用。

参考资料

  1. swift-unidirectional-flow - 使用最新的 Swift 泛型和 Swift 并发特性实现单向数据流。
  2. “Boundaries”, a talk by Gary Bernhardt from SCNA 2012

如何在 CI/CD 过程中实施高效的自动化测试和部署

1722516015057.jpg

摘要

在持续集成(CI)和持续交付(CD)过程中,自动化测试和部署是提高软件交付速度和质量的关键。本文将详细介绍如何选择适合的CI/CD工具,配置自动化构建和测试流程,制定全面的测试策略,并确保部署环境的稳定性,采用蓝绿部署等策略降低风险。

引言

持续集成和持续交付(CI/CD)是现代软件开发的最佳实践,旨在提高软件开发和发布的效率与质量。通过自动化测试和部署,可以减少人为错误,提升发布速度,并保障软件的稳定性。本文将通过具体的示例和代码,展示如何在 CI/CD 过程中实施有效的自动化测试和部署。

选择适合的 CI/CD 工具

常见 CI/CD 工具

目前市面上有很多优秀的 CI/CD 工具,例如:

  1. Jenkins
  2. GitLab CI/CD
  3. Travis CI
  4. CircleCI
  5. Azure DevOps

选择依据

选择CI/CD工具时,主要考虑以下因素:

  1. 与现有系统的兼容性:工具是否支持现有的代码库和工作流程。
  2. 扩展性和插件支持:工具是否支持多种插件和扩展,以满足各种需求。
  3. 社区和支持:工具的社区活跃度和官方支持情况。
  4. 费用:工具的成本是否在预算范围内。

配置自动化构建和测试流程

Jenkins示例

  1. 安装Jenkins

    # 安装Jenkins
    sudo apt-get update
    sudo apt-get install -y openjdk-11-jdk
    wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
    sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
    sudo apt-get update
    sudo apt-get install -y jenkins
    
  2. 配置Jenkins Pipeline 创建一个Jenkinsfile来定义流水线:

    pipeline {
        agent any
        stages {
            stage('Build') {
                steps {
                    echo 'Building...'
                    sh './gradlew build'
                }
            }
            stage('Test') {
                steps {
                    echo 'Testing...'
                    sh './gradlew test'
                }
            }
            stage('Deploy') {
                steps {
                    echo 'Deploying...'
                    sh './deploy.sh'
                }
            }
        }
    }
    

制定测试策略

单元测试

单元测试是测试的基础,主要用于验证单个功能模块的正确性。示例代码:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

JUnit 测试用例:

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculatorTest {
    @Test
    public void testAdd() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }
}

集成测试

集成测试用于验证多个模块之间的协作情况。示例代码:

// 使用Spring Boot进行集成测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Welcome")));
    }
}

系统测试

系统测试是对整个系统进行测试,确保系统在各种条件下都能正常工作。

确保部署环境的稳定性

蓝绿部署

蓝绿部署是一种无中断部署技术,可以显著降低生产环境的风险。通过保持两个相同的生产环境(蓝色和绿色),在绿色环境中部署新版本,然后切换流量到新版本。

  1. 配置Nginx进行蓝绿部署
    upstream blue {
        server blue.example.com;
    }
    
    upstream green {
        server green.example.com;
    }
    
    server {
        listen 80;
        server_name example.com;
    
        location / {
            proxy_pass http://green;  # 切换到green环境
        }
    }
    

未来展望

未来,我们可以进一步优化CI/CD流程,通过引入更多的自动化测试工具和方法,提高测试覆盖率和测试效率。同时,随着技术的发展,我们还可以探索更多先进的部署策略,如金丝雀部署等,进一步提升软件交付的质量和速度。

总结

本文详细介绍了如何在CI/CD过程中实施有效的自动化测试和部署。从选择合适的CI/CD工具、配置自动化构建和测试流程、制定全面的测试策略,到确保部署环境的稳定性,采用蓝绿部署等策略,本文提供了具体的示例代码和配置说明。

参考资料

  1. Jenkins 官方文档
  2. JUnit 官方文档
  3. Spring Boot Testing
  4. Nginx Blue-Green Deployment

自定义 SwiftUI 中符号图像的外观

1722425826929.jpg

前言

符号图像是来自 Apple的SF Symbols 库的矢量图标,设计用于在 Apple 平台上使用。这些可缩放的图像适应不同的大小和重量,确保在我们的应用程序中具有一致的高质量图标。在 SwiftUI 中使用符号图像非常简单,只需使用 Image 视图和所需符号的系统名称。下面是一个快速示例:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Image(systemName: "star")
    }
}

大小

尽管符号被放置在Image视图中,但它应被视为文本。要调整符号的大小,我们可以应用 font() 修饰符,就像在Text视图中一样。这使我们能够将符号的大小与不同的文本样式对齐,确保UI的视觉一致性。

HStack {
    Image(systemName: "star")
        .font(.title)
    
    Image(systemName: "star")
        .font(.body)
    
    Image(systemName: "star")
        .font(.caption)
}

我们可以使用 fontWeight() 修饰符来调整符号的重量。这个修饰符改变符号笔画的粗细,使我们能够将符号与周围的文本匹配或对比。

HStack {
    Image(systemName: "star")
        .fontWeight(.light)
    
    Image(systemName: "star")
        .fontWeight(.bold)
    
    Image(systemName: "star")
        .fontWeight(.black)
}

要根据字体大小相对缩放图像,我们应该使用 imageScale() 修饰符。有三个选项:小、中、大,它们根据字体大小按比例缩放符号。如果没有明确设置字体,符号将从当前环境中继承字体。

HStack {
    Image(systemName: "star")
        .imageScale(.small)
    
    Image(systemName: "star")
        .imageScale(.medium)
    
    Image(systemName: "star")
        .imageScale(.large)
}
.font(.headline)

不建议通过应用resizable()修饰符并设置框架来调整符号图像的大小,因为这样做会使图像停止作为符号图像,从而影响其与文本的布局和对齐。

颜色

使用SwiftUI中的foregroundStyle()视图修饰符,可以轻松自定义符号图像的颜色。这个修饰符允许我们直接设置符号图像的颜色。

Image(systemName: "star")
    .foregroundStyle(.orange)

foregroundStyle() 修饰符可以采用任何 ShapeStyle,包括渐变,这为我们的符号图像提供了广泛的自定义可能性。在这个例子中,星形符号使用了从黄色到红色的线性渐变,从顶部到底部过渡。

Image(systemName: "star")
    .foregroundStyle(
        LinearGradient(
            colors: [.yellow, .red],
            startPoint: .top,
            endPoint: .bottom
        )
    )

渲染模式

我们可以通过使用不同的渲染模式进一步自定义符号图像的外观。SF Symbols有四种不同的渲染模式,这些模式会改变符号的颜色和外观。一些渲染模式使整个图标保持相同颜色,而其他模式则允许多种颜色。

要在SwiftUI中设置符号图像的首选渲染模式,我们使用 symbolRenderingMode() 修饰符。

单色

单色是默认的渲染模式。在这种模式下,符号的每一层都是相同的颜色。

Image(systemName: "thermometer.snowflake")
    .symbolRenderingMode(.monochrome)

分层

分层模式将符号渲染为多个层,每层应用不同的不透明度。层次结构和不透明度在每个符号中是预定义的,但我们仍然可以使用 foregroundStyle() 修饰符自定义颜色。

HStack {
    Image(systemName: "thermometer.snowflake")
    Image(systemName: "thermometer.snowflake")
        .foregroundStyle(.indigo)
}
.symbolRenderingMode(.hierarchical)

symbolRenderingMode() 修饰符既可以直接应用于图像视图,也可以通过将其应用于包含多个符号图像的父视图来在环境中设置。这样,父元素内的所有符号图像都会受到影响。

调色板

调色板模式允许符号以多层呈现,每层具有不同的颜色。这种模式非常适合创建色彩丰富的多层图标。

Image(systemName: "thermometer.snowflake")
    .symbolRenderingMode(.palette)
    .foregroundStyle(.blue, .teal, .gray)

我们不需要显式地指定调色板呈现模式。如果我们在 foregroundStyle() 修饰符中应用多个样式,则调色板模式将自动激活。

Image(systemName: "thermometer.snowflake")
    .foregroundStyle(.blue, .teal, .gray)

如果我们为一个定义了三个层次结构的符号指定两种颜色,那么第二层和第三层将使用相同的颜色。

Image(systemName: "thermometer.snowflake")
    .foregroundStyle(.blue, .gray)

多色

多色模式使用由 Apple 定义的一组固定颜色渲染符号。在使用多色渲染时,我们无法自定义符号的颜色,它将使用预定义的颜色。

HStack {
    Image(systemName: "thermometer.snowflake")
    Image(systemName: "thermometer.sun.fill")
}
.symbolRenderingMode(.multicolor)

值得注意的是,由于这些颜色是固定的,它们不适应明暗模式。例如,我们的温度计符号具有白色轮廓,在白色背景上是不可见的。

并非所有符号都支持每种呈现模式。图层较少的符号在不同模式下看起来可能相同,分层和调色板模式看起来类似于单色。

可变值

在 SwiftUI 中显示符号图像时,我们可以提供一个 0.0 到 1.0 之间的可选值,渲染的图像可以使用它来自定义外观。如果符号不支持可变值,此参数无效。我们应该在 SF Symbols 应用程序中检查哪些符号支持可变值。

HStack {
    Image(systemName: "speaker.wave.3", variableValue: 0)
    Image(systemName: "speaker.wave.3", variableValue: 0.3)
    Image(systemName: "speaker.wave.3", variableValue: 0.6)
    Image(systemName: "speaker.wave.3", variableValue: 0.9)
}

可变值可以表示一个随着时间变化的特性,例如容量或强度。这使得符号的外观可以根据应用程序的状态动态变化。

struct ContentView: View {
    @State private var value = 0.5
    
    var body: some View {
        VStack {
            Image(
                systemName: "speaker.wave.3",
                variableValue: value
            )
            Slider(value: $value, in: 0...1)
                .padding()
        }
        .padding()
    }
}

在这个例子中,符号 speaker.wave.3 根据 Slider 提供的值改变其外观。

我们应该使用可变值来传达状态的变化,例如音量、电池电量或信号强度,为用户提供动态状态的清晰视觉表示。为了传达深度和视觉层次,我们应该使用分层渲染模式,它可以提升某些图层,并区分符号内的前景和背景元素。

设计变体

符号可以有不同的设计变体,例如填充和斜杠,以帮助传达特定的状态和操作。斜杠变体可以表示项目或操作不可用,而填充变体可以表示选择。

在 SwiftUI 中,我们可以使用 symbolVariant() 修饰符来应用这些变体。

HStack {
    Image(systemName: "heart")
    
    Image(systemName: "heart")
        .symbolVariant(.slash)
    
    Image(systemName: "heart")
        .symbolVariant(.fill)
}

不同的符号变体用于各种设计目的。轮廓变体在工具栏、导航栏和列表中非常有效,而填充变体则用于强调选择的状态。

HStack {
    Image(systemName: "heart")
        .symbolVariant(.circle)
    
    Image(systemName: "heart")
        .symbolVariant(.square)
    
    Image(systemName: "heart")
        .symbolVariant(.rectangle)
}

不同的符号变体具有不同的设计用途。轮廓变体在工具栏、导航栏和列表中非常有效,因为这些地方通常会与文本一起显示符号。将符号封装在圆形或方形等形状中可以增强其可读性,特别是在较小尺寸下。填充变体由于其实心区域,使符号更具视觉强调性,非常适合用于 iOS 标签栏、滑动操作以及指示选择的强调颜色场景。

在许多情况下,显示符号的视图会自动选择合适的变体。例如,iOS 标签栏通常使用填充变体,而导航栏则偏好轮廓变体。这种自动选择确保符号在不同上下文中有效使用,而无需明确指定。

示例代码

import SwiftUI

struct ContentView: View {
    @State private var value = 0.5
    
    var body: some View {
        VStack {
            Image(
                systemName: "speaker.wave.3",
                variableValue: value
            )
            .symbolRenderingMode(.hierarchical)
            .foregroundStyle(.blue)
            Slider(value: $value, in: 0...1)
                .padding()
        }
        .padding()
    }
}

运行 Demo

  1. 打开Xcode并创建一个新的 SwiftUI 项目。
  2. 将上述代码粘贴到 ContentView.swift 文件中。
  3. 运行项目,查看效果。

结论

在SwiftUI中增强符号图像可以显著改善应用程序的外观和感觉。通过调整大小、颜色、渲染模式、可变值和设计变体,我们可以创建使应用程序更直观和视觉吸引力的图标。SwiftUI使这些调整变得简单易行,使我们能够轻松实现和改进这些自定义以提供更好的用户体验。

❌