阅读视图

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

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 语言的发展贡献自己的力量。

苹果仍在研发更大尺寸的 iMac | Swift 周报 issue 60

前言

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

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

野心太大,才华甚少。于是,回忆间遗憾,梦境中恐惧,时光里迷惘。Swift社区于流年里筑基,岁月间化形,恍惚中蜕变。所以,千日苦修,终成不朽!👊👊👊

周报精选

新闻和社区:消息称苹果仍在研发更大尺寸的 iMac 屏幕超过 30 英寸

提案:允许推断 TaskGroup 的 ChildTaskResult 类型提案通过审查

Swift 论坛:讨论真实应用中的 Swift 并发

推荐博文:在 SwiftUI 中追踪几何变化

话题讨论:

你希望 Apple 更加关注 AI 开发的哪个领域?

新闻和社区

消息称苹果仍在研发更大尺寸的 iMac 屏幕超过 30 英寸

2024 年 8 月 15 日

据外媒报道,在转向自研的M系列芯片之后,苹果公司 2021 年 4 月份推出的搭载 M1 芯片和 2023 年 10 月份推出的搭载 M3 芯片的 iMac,都是 24 英寸的屏幕,而随着 iMac Pro 和搭载英特尔芯片的 2020 年款 iMac 的下架,iMac 也就有一段没有了 27 英寸版可买,只有 24 英寸屏幕可选。

虽然苹果目前在售的 iMac 只有 24 英寸屏幕,但从去年开始,多次有外媒在报道中称苹果在研发更大尺寸的 iMac,在去年年中,就曾有消息称一款更大尺寸的 iMac,已在研发的早期阶段。不过到目前为止,尚未有更大屏幕的 iMac 推出,去年年底推出的仍是 24 英寸屏幕。

但长期关注苹果的一名资深记者透露,苹果公司仍在探索屏幕尺寸更大的 iMac。

不过,这名长期关注苹果的资深记者,目前还不确定更大尺寸的 iMac 是否会搭载苹果自研的 M4 芯片,也就是 5 月份推出的新一代 iPad Pro 率先搭载的那一款芯片。

此外,基于不同的芯片,外媒目前在更大尺寸的 iMac 的推出时间上也还有不同的看法,搭载 M4 芯片,可能就会同 MacBook Air、Mac Pro、Mac Studio 一样,在明年推出,但苹果也有可能等待 M5 芯片,推出时间可能就是在 2026 年或之后。

值得注意的是,在去年 10 月份,知名苹果产品分析师郭明錤,曾预计苹果在 2025 年将推出 32 英寸屏幕的 iMac,搭载 mini-LED 显示屏。

至于苹果是否会推出及何时推出屏幕超过 30 英寸的 iMac,要在他们正式发布之后才会揭晓,保密传统浓厚的他们,在发布之前不会公布相关的消息。(来源:TechWeb)

最新!苹果大动作

2024 年 8 月 14 日

面对欧盟等地监管方多年的压力,苹果公司终于低头。

美东时间 8 月 14 日周三,苹果宣布,从新版操作系统 iOS 18.1 开始,开发者将可使用 iPhone 内的安全元件(SE),不通过苹果旗下支付和钱包 Apple Pay 和 Apple Wallet,在开发者自行开发的 iPhone 应用程序 App 中,提供 NFC 无接触数据交换功能。

要使用这些 App 内的新功能,用户可以直接打开 App,也可以在 iOS 设置中将该 App 设置为默认支持,然后双击 iPhone 侧边按钮,即可发起交易。

image.png

苹果称,通过新的 NFC 和 SE API,开发者将能提供 App 内的无接触数据交换,可用于闭环公交、企业工牌、学生证、家门钥匙、酒店钥匙、商家积分和回馈卡,甚至活动门票等,未来还将支持身份证件。这些 API 将首先通过即将发布的 iOS 18.1 开发者资源向澳大利亚、巴西、加拿大、日本、新西兰、英国和美国的开发者提供,未来将落地更多地区。

同时苹果指出,若要在 iPhone App 中纳入上述新的无接触数据交换功能,开发者需要同苹果签订一份商业协议,申请并得到NFC和SE授权,且支付相关费用。这将确保开发者符合特定行业及监管的要求,并遵守苹果的安全和隐私标准。

上述苹果公告意味着,苹果将开始允许第三方使用 iPhone 的支付芯片来处理交易,等于允许银行和其他服务方与 Apple Pay 平台竞争。有消息称,到目前为止,只有 Apple Pay 和 Apple Wallet 可以使用 iPhone NFC 芯片的许多功能,这种“独家”优势将随着 iOS 18.1 上线而改变。

值得一提的是,今年苹果在欧盟反垄断压力下作出了一系列让步。

今年 1 月苹果宣布,对其在欧盟的 iOS、Safari 和 App 应用商店产品进行一系列历史性的大幅改革。改革将首次允许客户从苹果应用商店 App Store 以外下载软件。人们还将能够使用其他的支付系统,并且更容易地选择新的默认网络浏览器。

另据新华社报道,欧盟委员会 7 月 11 日宣布已同美国苹果公司达成和解,苹果承诺向竞争对手免费开放基于近场通信(NFC)技术的移动支付功能。相关承诺将具法律约束力,为期 10 年,适用于整个欧洲经济区。

2022 年,欧盟委员会指控苹果公司限制第三方移动支付应用开发者使用 NFC,这使苹果支付在和其他同类产品竞争中获得不公平的优势。

据欧盟委员会介绍,此前苹果提出初步承诺,向第三方移动支付应用开发者免费开放在苹果设备上的 NFC 访问权限,无需通过苹果支付或苹果钱包等。此后,欧盟委员会对苹果承诺的措施进行了市场测试,苹果也根据测试和反馈结果修改了其承诺。

欧盟委员会认可了这些承诺并表示,苹果的最终承诺将有助于消除该委员会对相关领域市场竞争的担忧。

美国媒体援引苹果公司一份声明说,该公司将为欧洲经济区的开发者在他们的相关应用程序中提供多种使用场景下启用 NFC 非接触式支付和交易的选项。(来源:每日经济新闻)

Apple Entrepreneur Camp 现已开放申请

2024 年 8 月 13 日

Apple Entrepreneur Camp 旨在为少数群体创业者和开发者提供支持,并鼓励这些企业家在技术领域不断探索并取得持续发展。课程期间会提供一对一编程指导以及与 Apple 工程师和专家沟通的宝贵机会,参与者也将成为持续扩大的全球 Apple Entrepreneur Camp 营友网络中的一员。

申请通道现已面向女性*、黑人、西班牙裔/拉丁裔和原住民创业者及开发者开放。今年,我们很高兴能再次在库比提诺的 Apple 园区举办现场活动。对于无法亲赴现场参加的人员,我们仍将提供完整的在线课程。欢迎经营 App 驱动型企业的成熟企业家进一步了解资格要求并提交申请。

申请截止日期为太平洋时间 2024 年 9 月 3 日。

提案

通过的提案

SE-0440 DebugDescription 宏 提案通过审查。该提案已在 第五十七期周报 正在审查的提案模块做了详细介绍。

SE-0441 正式化“语言模式”术语 提案通过审查。该提案已在 第五十八期周报 正在审查的提案模块做了详细介绍。

SE-0442 允许推断 TaskGroup 的 ChildTaskResult 类型 提案通过审查。该提案已在 第五十九期周报 正在审查的提案模块做了详细介绍。

Swift论坛

  1. 提议并发安全通知

内容大概

该提案旨在将 Swift 并发引入到 NotificationCenter 中,以提高代码的安全性和健壮性。目前,NotificationCenter API 通过发布和观察通知的模式,使代码解耦。这种模式在 macOS、iOS 以及其他基于 Darwin 的系统中的框架中得到了广泛集成。通知的发布者通过 Notification.Name 标识发送通知,并可以选择性地包括 object 和 userInfo 作为负载。观察者则通过注册代码块或闭包来接收通知,并可以选择指定 OperationQueue 来执行这些观察者的代码。

然而,目前的 NotificationCenter 存在一些问题。首先,通知的并发性依赖于隐式约定,观察者的代码块通常会在与发布者相同的线程上运行。为了确保并发性,客户端通常需要查阅文档或使用并发机制,这可能会导致问题。此外,现有的通知类型和负载类型并不够强,使用字符串作为标识符容易导致拼写错误,且客户端在处理通知负载时,可能需要频繁地进行类型转换。

为了解决这些问题,提案提出了一个新协议 NotificationCenter.Message,该协议允许创建可以通过 NotificationCenter 发布和观察的类型,并提供对 Swift 并发的支持,同时保留与现有 Notification 类型的互操作性。通过定义 Notification.Name,NotificationCenter.Message 可以与现有的 Notification 类型兼容。默认情况下,符合 NotificationCenter.Message 的类型的观察者将在 MainActor 上运行,并且可以指定其他的隔离上下文。

提案还介绍了如何在 NotificationCenter.Message 与现有的 Notification 类型之间进行转换,例如通过定义 makeMessage(_:) 方法将通知转换为 NotificationCenter.Message,或通过 makeNotification(_:) 方法将 NotificationCenter.Message 转换为现有的 Notification 类型,以支持现有的 Objective-C 代码中的观察者。

提案的一个示例展示了如何将现有的 NSWorkspace.willLaunchApplicationNotification 通知适配为使用 NotificationCenter.Message,并展示了如何在客户端代码中观察和发布这样的通知。

该提案不仅增强了类型安全性和并发支持,还通过平滑的过渡路径确保了与现有代码库的兼容性。

  1. 讨论真实应用中的 Swift 并发

内容大概

在实际应用中使用 Swift 并发可能会带来一些复杂性和挑战。作者分享了一个自定义 NSTableColumn 的代码示例,该示例使用图片而不是字符串作为列头。在实现过程中,作者遇到了与 Swift 并发相关的问题,特别是在 Xcode 16 beta 5 中,某些以前可行的方法突然失效了。

代码示例如下:

final class LockTableColumn: NSTableColumn {
    lazy private var _headerCell: NSTableHeaderCell? = nil

    override var headerCell: NSTableHeaderCell {
        get {
            if _headerCell != nil { 
               return _headerCell 
            }

            _headerCell = super.headerCell // 保留父类提供的默认样式
            if let image = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: nil) {
                image.size = CGSize(width: 14, height: 14)
                Task { @MainActor [_headerCell] in
                    _headerCell!.image = image
                 }
            }
            return _headerCell
        }
        set {
            // 空操作
        }
    }
}

作者提到,NSTableColumn 并没有被标记为可发送(sendable),而 NSCell 则被绑定到 MainActor 上,这使得这两者的结合使用变得困难。特别是,当尝试在代码中使用 Task { @MainActor in } 来设置图片属性时,编译器会抛出错误,提示任务或 actor 隔离值无法发送。为了解决这个问题,作者必须使用捕获列表 [ _headerCell ],但这一点并不直观,特别是对于初学者来说。

作者还指出,Swift 并发的严格性导致了一些简单任务的实现变得异常复杂,并质疑当前 Swift 并发的成熟度和苹果框架的准备情况。不断变化的开发环境(如 beta 版本之间的差异)进一步增加了学习和采用 Swift 并发的难度。

此外,作者讨论了在大型、旧项目中使用 Swift 并发的挑战,特别是在尝试迁移到 Swift 6 时遇到的困难。虽然迁移可能是一个长期的过程,但作者认为,尽早采用新特性比等待其完全成熟更为可取。

总之,尽管 Swift 并发在理论上提供了更好的安全性,但在实践中,它可能会增加开发的复杂性,特别是在现有代码库中。

  1. 提议未实现函数的占位符

内容大概

讨论了对未实现函数的占位符进行改进的提案。提案的核心思想是引入一种新的语法,用于明确标记未实现的函数或方法。这种语法将帮助开发者在编写和维护代码时更清楚地识别出哪些部分尚未完成,从而减少遗漏和错误。

提案中提出了以下几个关键点:

  1. 建议添加一个新的关键字或标记,来表示一个函数或方法尚未实现。这种标记可以使代码在编译时产生警告或错误,提醒开发者注意未完成的部分。

  2. 通过这种标记,开发者可以在代码中添加详细的注释或文档,说明该函数将来会实现的功能。这有助于团队成员之间的沟通,并且在代码审查过程中提供更多的信息。

  3. 提案中提供了几个具体的语法示例,展示如何使用这种新语法标记和处理未实现的功能。这些示例展示了不同情况下的用法,并说明了这种方法如何提高代码的可读性和可维护性。

总的来说,这项提案旨在提高代码的清晰度和可靠性,帮助开发者更有效地管理未实现的功能。

  1. 讨论测试基于闭包的异步 API

内容大概

在XCTest中,当设置一个非零超时时间时,fulfillment(of:timeout:) API 会旋转运行循环并等待最长指定时间,直到 XCTestExpectation 被满足。相对而言,Swift Testing 中的 confirmation() API 不会等待,它要求 Confirmation 在闭包返回之前得到确认。

在实际应用中,start() 函数创建了一个无结构的Task,但没有等待其值,这意味着当 start() 返回时,任务中的异步操作可能尚未完成。为了解决这个问题,可以修改代码,让 start() 函数返回一个 Task,并在 confirmation() 闭包中等待该任务完成。具体做法是:

@MainActor
@discardableResult
func start() -> Task<...> {
    Task {
        // 异步操作
    }
}

然后在 confirmation 闭包中等待任务完成:

await confirmation { confirmed in
    env.analytics.trackEventMock.whenCalled { _ in
        confirmed()
    }
    let startingTask = await sut.start()
    await startingTask.value
}

虽然这种方法在理论上可行,但实际应用中受到限制,特别是当需要创建与并发模型无关的 ViewModel 时。在这种情况下,ViewModel 通常具有一个同步接口,并且只从视图层访问。视图通过该接口向 ViewModel 发送信号,ViewModel 启动一个 Task,或者在旧代码中使用 Combine 或传统的闭包 API。当异步操作完成时,ViewModel 会更新状态并通过 @Published 属性或 Observation 框架将其传递到视图层。

由于 Swift Testing 的确认机制无法正常工作,这使得采用该框架变得困难。工程师们可能只能在新项目中使用该框架,而不能在现有项目中轻松集成。

  1. 讨论ShapedArray 中 4D 及更高维度的下标

内容大概

讨论中,有关 ShapedArray 的子脚本功能的扩展请求涉及了几个关键方面:

  1. 当前,ShapedArray 可以处理一维、二维和三维数组的索引和子脚本操作。这意味着对于这些维度的数据,用户可以通过索引轻松地访问和修改元素。然而,对于四维及更高维度的数组,现有的 ShapedArray 实现尚不支持直接的子脚本操作。

  2. 用户希望能够对更高维度的数组进行类似的一维、二维、三维数组那样的子脚本操作。这种需求通常来源于需要处理复杂的数据结构,如多维矩阵或张量,这在科学计算、机器学习和图像处理等领域非常常见。

  3. 讨论中建议通过扩展 ShapedArray 的子脚本功能,允许对四维及更高维度的数组进行直观的访问。例如,能够通过多个索引进行访问,如 array[x][y][z][w],其中每个索引对应数组的不同维度。这将使得操作这些复杂数据结构变得更加简洁和高效。

  4. 扩展子脚本功能以支持更高维度数组面临一些技术挑战,包括:

    • API设计: 需要设计一个易于理解和使用的API,同时支持灵活的维度访问。
    • 性能考虑: 高维数组的操作可能会涉及大量数据,如何优化性能以确保高效的访问和操作是一个重要问题。
    • 兼容性: 确保新的功能不会破坏现有的ShapedArray实现,并且能够与现有代码库兼容。

综上所述,扩展 ShapedArray 以支持四维及更高维度的子脚本操作被认为是一个有价值的改进,能够显著提升处理复杂数据结构的灵活性和效率。然而,实施这一改进需要解决若干技术挑战,并考虑如何设计一个用户友好的 API。

推荐博文

深入探究 Swift 中 String 的内存布局及底层实现

摘要: 这篇博客深入探讨了 Swift 中的 String 类型的内存布局和底层实现。文章通过查看内存、汇编代码及 Swift 源码,详细分析了 String 的内部结构。主要内容包括:

空字符串:String 内部有一个 _StringGuts 结构体,包含 _StringObject 成员,_StringObject 持有一个 Builtin.BridgeObject 类型的 _object 和一个 UInt64 类型的 _countAndFlagsBits

小字符串:当字符串长度不超过 15 时,字符串内容直接存储在变量地址中,使用 16 个字节存储,前 15 个字节存储字符,最后 1 个字节存储长度和标志位。

大字符串:当字符串长度超过 15 时,字符串变量的内存布局发生变化,地址中的部分字节存储字符串长度,另一部分存储字符串内容的地址。_object 字段通过位操作和偏移量管理字符串的实际存储地址。

平台差异:文章也讨论了 64 位、32 位和 16 位平台上的不同内存布局,并结合 Mach-O 文件分析了字符串在内存中的位置。

最终,文章总结了 Swift 字符串的内存布局:在 64 位平台上, String 占用 16 个字节,长度小于等于 15 的字符串直接存储在这 16 字节中。

Swift 开发新高度:自己动手实现 Optional 类型

摘要: 这篇文章讲述了如何自己实现 Swift 中的 Optional 类型。作者介绍了 Swift 内置的 Optional 是一个枚举类型,具有 some 和 none 两个 case,并使用泛型来处理不同类型的数据。作者随后展示了如何定义一个自定义的 Optional 类型 CustomOptional,并为其添加了方法来访问、解包值,以及通过 map 和 flatMap 方法实现可选链。通过这些步骤,读者可以更深入地理解 Swift Optional 的底层实现和代数数据类型的强大功能。

在 SwiftUI 中追踪几何变化

摘要: 这篇博客介绍了如何在 SwiftUI 中使用新的 onGeometryChange 修饰符来追踪视图的几何变化。作者详细说明了 onGeometryChange 的三个参数:可观察的结果类型、用于几何转换的闭包,以及处理转换结果的闭包。作者提供了多个示例,展示了如何在 ScrollView 中追踪视图的尺寸和位置变化,并强调了该修饰符对性能优化的重要性。

话题讨论

你希望 Apple 更加关注 AI 开发的哪个领域?

  1. 增强机器学习模型和工具
  2. 更好地将 AI 与 Swift 和 Xcode 集成
  3. 更多 AI 开发资源和教程
  4. 改进 AI 的隐私和安全功能
  5. 其他(用户输入)

关于我们

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使这些调整变得简单易行,使我们能够轻松实现和改进这些自定义以提供更好的用户体验。

SwiftUI 中掌握 ScrollView 的使用:滚动可见性

前言

我们的滚动 API 中又有一个重要的新增功能:滚动可见性。现在,你可以获取可见标识符列表,或者快速检查并监控 ScrollView 内视图的可见性状态。本周,我们将学习如何使用新的 onScrollTargetVisibilityChangeonScrollVisibilityChange 视图修饰符。

视图修饰符

让我们先从 onScrollTargetVisibilityChange 视图修饰符开始。它设计得易于使用,允许你将其附加到具有滚动目标布局的任何 ScrollView 上。让我们通过一个示例来探讨这个修饰符的使用。

struct ContentView: View {
    @State private var visible: [Int] = []
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1..<100, id: \.self) { item in
                    Text(verbatim: item.formatted())
                }
            }
            .scrollTargetLayout()
        }
        .onScrollTargetVisibilityChange(idType: Int.self) { identifiers in
            visible = identifiers
        }
        .onChange(of: visible) {
            print(visible)
        }
    }
}

如上例所示,我们在懒加载栈(LazyVStack)上使用 scrollTargetLayout 视图修饰符,以便允许 ScrollView 针对栈的子视图进行目标识别,而不是针对栈本身。

要了解有关 scrollTargetLayout 视图修饰符的更多信息,请查看我的文章《掌握 SwiftUI 中的 ScrollView:滚动几何》。

应用场景

我们还将 onScrollTargetVisibilityChange 视图修饰符附加到 ScrollView 上,提供标识符类型和操作闭包。在操作闭包内,我们获取可见标识符列表,并可以对可见项执行所需的操作。

有时,视图需要在其可见性状态在 ScrollView 中发生变化时进行响应。对于这些情况,SwiftUI 框架引入了 onScrollVisibilityChange 视图修饰符,你可以将其附加到 ScrollView 内的任何视图上以处理其可见性。

struct VideoPlayerView: View {
    let url: URL
    
    @State var player: AVPlayer?
    
    var body: some View {
        VideoPlayer(player: player)
            .task {
                if player == nil {
                    player = AVPlayer(url: url)
                }
            }
            .onScrollVisibilityChange { isVisible in
                if isVisible {
                    player?.play()
                } else {
                    player?.pause()
                }
            }
    }
}

上例定义了 VideoPlayerView 视图,该视图在其可见时自动播放视频内容。正如你所见,我们将 onScrollVisibilityChange 视图修饰符附加到视图本身,并提供一个操作闭包。我们在操作闭包内获得可见性参数,并可以对其变化进行响应。

可见性

onScrollVisibilityChangeonScrollTargetVisibilityChange 修饰符都具有 threshold 参数。threshold 参数允许我们调整需要可见的视口部分的数量,以触发操作闭包。默认情况下,SwiftUI 框架使用 0.5 作为阈值,这意味着至少 50% 的视图需要可见,SwiftUI 才会运行操作。但你可以轻松调整此值。

struct VideoPlayerView: View {
    let url: URL
    
    @State var player: AVPlayer?
    
    var body: some View {
        VideoPlayer(player: player)
            .task {
                if player == nil {
                    player = AVPlayer(url: url)
                }
            }
            .onScrollVisibilityChange(threshold: 0.1) { isVisible in
                if isVisible {
                    player?.play()
                } else {
                    player?.pause()
                }
            }
    }
}

在上述示例中,我们定义了阈值,这意味着 SwiftUI 将在视图至少有 10% 可见时运行操作闭包。同样,当视图从可见状态转换为不可见状态,即显示的视口部分少于 10% 时,也会运行该闭包。

完整示例

上面对视图修饰符有了初步了解,它的设计得易于使用,允许你将其附加到具有滚动目标布局的任何 ScrollView 上。让我们通过一个示例来探讨这个修饰符的使用。

示例代码如下:

import SwiftUI
import AVKit

struct ContentView: View {
    @State private var visible: [Int] = []
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1..<100, id: \.self) { item in
                    Text(verbatim: item.formatted())
                        .frame(width: 100, height: 100)
                        .background(item % 2 == 0 ? Color.blue : Color.red)
                        .cornerRadius(10)
                        .padding(5)
                }
            }
            .scrollTargetLayout()
        }
        .onScrollTargetVisibilityChange(idType: Int.self) { identifiers in
            visible = identifiers
        }
        .onChange(of: visible) { newVisible in
            print("Visible items: \(newVisible)")
        }
        .navigationTitle("ScrollView Demo")
    }
}

struct VideoPlayerView: View {
    let url: URL
    
    @State var player: AVPlayer?
    
    var body: some View {
        VideoPlayer(player: player)
            .frame(height: 200)
            .task {
                if player == nil {
                    player = AVPlayer(url: url)
                }
            }
            .onScrollVisibilityChange { isVisible in
                if isVisible {
                    player?.play()
                } else {
                    player?.pause()
                }
            }
    }
}

@main
struct ScrollViewVisibilityApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                VStack {
                    ContentView()
                    Spacer()
                    VideoPlayerView(url: URL(string: "https://www.example.com/video.mp4")!)
                        .padding()
                }
            }
        }
    }
}

这个示例 Demo 展示了如何使用 onScrollTargetVisibilityChangeonScrollVisibilityChange 视图修饰符来跟踪 ScrollView 中的视图可见性。整个示例分为两个部分:一个是显示带有多个文本视图的 ScrollView,另一个是显示一个视频播放器视图。

ContentView

  1. ScrollView 和 LazyVStack:使用 ScrollView 包裹一个 LazyVStack,在其中放置 1 到 99 的数字。每个数字都显示在一个 Text 视图中,并有不同的背景颜色。
  2. scrollTargetLayout:在 LazyVStack 上应用 scrollTargetLayout 视图修饰符,以允许 ScrollView 针对栈的子视图进行目标识别。
  3. onScrollTargetVisibilityChange:在 ScrollView 上应用 onScrollTargetVisibilityChange 视图修饰符,并提供标识符类型和操作闭包。在操作闭包内,获取可见标识符列表并赋值给 visible 状态变量。
  4. onChange:监听 visible 状态变量的变化,并打印当前可见的项。

VideoPlayerView

  1. VideoPlayer:定义一个视频播放器视图,使用 AVPlayer 播放视频。
  2. task:在 task 修饰符中初始化播放器。
  3. onScrollVisibilityChange:在视频播放器视图上应用 onScrollVisibilityChange 视图修饰符,并提供一个操作闭包。在操作闭包内,根据可见性状态来播放或暂停视频。

ScrollViewVisibilityApp

  1. 主应用入口:定义主应用入口 ScrollViewVisibilityApp,将 ContentViewVideoPlayerView 组合到一个垂直堆栈中,并通过 NavigationView 进行导航。

运行这个 Demo,你会看到一个带有多个文本视图的 ScrollView,当你滚动时,控制台会打印当前可见的项。此外,在页面底部有一个视频播放器,当视频播放器出现在视口内时,它会自动播放,当其离开视口时,会自动暂停。

总结

今天,我们学习了如何跟踪 ScrollView 内特定视图的可见性,并监控可见标识符列表。示例展示了如何使用 SwiftUI 的滚动可见性修饰符来增强用户体验和交互性。希望能对你有所帮助。

苹果将为 Apple Watch X 铺路 | Swift 周报 issue 45

前言

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

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

我能有什么办法,失去和拥有都由不得我.Swift社区也和你一样伤心无奈,但新的一年还是要积极坚强的生活!

周报精选

新闻和社区:苹果或将扩充健康版图,为 Apple Watch X 铺路

提案:推断方法和关键路径文本的 Sendable 提案通过审查

Swift 论坛:讨论在循环中初始化强制属性的推荐方法?

推荐博文:手把手教你用 Swift 实现命令行工具

话题讨论:

过去的 2023 年你完成了哪些目标?

上期话题结果

根据投票结果可以看出在晋升过程中,组内成员普遍认为实际项目贡献至关重要。然而,领导风格、人际关系以及对未来发展的长远考量也受到了一定程度的重视。

新闻和社区

苹果或将扩充健康版图,为Apple Watch X铺路

全球市值最大的科技公司 + 前全球最大社交健身公司,会碰撞出什么样的火花?想想就能脑补出一部强强联合横扫全球的励志热血片。

上周,深水资产管理公司(Deepwater Asset Management)发布了 2024 年预测报告,其中一项预测就是苹果收购派乐腾(Peloton)公司。深水作为全球知名的资产管理公司,其预测报告还是有一定的可信度的。

大家对苹果肯定很熟悉了,那这个派乐腾又是何方神圣呢?派乐腾主营业务是销售可以联网的健身器材硬件,以及提供付费订阅的流媒体健身课程,在巅峰时期还加设了服装产品线。在 2020 年乘着「居家经济」的东风,派乐腾的股价频频上涨,屡创历史新高,一度登上了全球最大社交健身公司的宝座。

深水资产管理公司预测,如果苹果收购派乐腾,除了能通过健身器材来完善智能手表和健身追踪软件外,还能为苹果的订阅收入增加约 17 亿美元(约合人民币 121.38 亿元)。

首先,从品牌层面上看,派乐腾在居家健身领域无疑是先行者,凭借健身器材与订阅课程相结合的模式强化了用户的体验感,其中「大屏观感」是许多用户选择派乐腾的直接原因。如果完成收购,派乐腾的品牌效应会直接提升苹果在健康领域的影响力,收获大批经过筛选的忠实用户。

Apple Watch X,收购案的最大收益者。医疗健康将是苹果未来的发展方向,想不到还有其他什么产品比它更重要。这是苹果首席运营官威廉姆斯(Jeff Williams)在一场公开活动上发表的原话。

回顾苹果健康业务 9 年的发展历程,战略上主要分为两个方向。一是打造运动和健身功能吸引消费者;二是与传统医疗系统或医学机构开展合作。

Apple Watch和iPhone则是苹果健康业务的重要载体,负责给用户提供健康和健身领域的功能。其中大部分的功能都是围绕 Apple Watch 展开的,包括心率测量、睡眠监控、女性健康等。2022 年公布的 iOS 16 和 watchOS 9 中,就涵盖了多达 17 种健康功能,如今更是加入了心理健康、视力健康。

因此此时传出苹果收购派乐腾的风声,相信很可能是苹果为 Apple Watch 发布十周年之作做准备。

早在去年 1 月,就有某供应链透露消息,指苹果将在 Apple Watch 发布十周年推出类似于 iPhone X 的重大设计调整。大意是跳过 Apple Watch Series 9,直接发布 Apple Watch X。

据彭博社科技记者马克·古尔曼(Mark Gurman)报道,Apple Watch X 有望成为迄今为止 Apple Watch 最大的一次更新。有望采用更薄的表壳和新的表带,磁吸设计的表带连接方式,以及 Micro LED 显示屏。

当然,无论 Apple Watch X 在产品方面有多大调整,始终觉得智能功能才是智能手表区别于传统手表的核心。收购派乐腾将补充 Apple Watch 在健身器材上的空缺,用户在运动监测类型上的选择将更加丰富。两者建立连接,也会在一定程度上提升用户体验感。

更新后的《Apple Developer Program 许可协议》现已发布

2023 年 12 月 22 日

《Apple Developer Program 许可协议》已经过修订,以便为更新后的政策提供支持,并对相关内容做出阐释。具体修订内容包括:

  • 定义,第 3.3.3 (N) 节:将“Tap to Present ID”更新为“ID Verifier (证件验证系统)”
  • 定义,第 14.10 节:更新了有关管辖法律和地点的术语
  • 第 3.3 节:为了清晰起见,对条款进行了重新组织和分类
  • 第 3.3.3 (B) 节:阐释了隐私和第三方 SDK
  • 第 6.7 节:更新了有关分析的条款
  • 第 12 节:阐释了保修免责声明
  • 附件 1:更新了 Apple 推送通知服务和本地通知的使用条款
  • 附件 9:Apple Developer Program 会员资格中包含 Xcode Cloud 计算时间的更新条款

提案

通过的提案

SE-0418 推断方法和关键路径文本的 Sendable 提案通过审查。该提案已在 四十四期周报 正在审查的提案模块做了详细介绍。

SE-0416 键路径文字作为函数的子类型 提案通过审查。该提案已在 四十四期周报 正在审查的提案模块做了详细介绍。

Swift论坛

  1. 提议概括“AsyncSequence”和“AsyncIteratorProtocol”

文章详细介绍了 AsyncSequence 和 AsyncIteratorProtocol 的两个基本问题的解决方案:定制和限制的 @rethrows 属性,以及迭代 AsyncSequence 时的 Sendable 检查问题。 这包括在 提议-并发模块中的类型化抛出中描述的 AsyncSequence 和 AsyncIteratorProtocol 中采用类型化抛出,以及采用隔离参数,以便 AsyncSequenceAsyncIteratorProtocol 在抛出的错误类型和参与者隔离上都是多态的。

您可以在 GitHub 上查看提案草案,网址为 swift-evolution/proposals/NNNN-generalize-async-sequence.md

  1. 讨论在循环中初始化强制属性的推荐方法?

内容概括 讨论的目的是寻求有关在循环等迭代过程中初始化 Swift 结构中的强制属性 (let) 的建议。 他们尝试在结构体的 init() 函数中使用 while 循环,但由于编译器要求在退出初始化程序之前初始化所有属性而遇到错误。

struct Bing {
    let bang: String

    init() {
        while true {
            // do something iterative

            if /* some condition */ {
                bang = "bong"
                break
            }
        }

    }
}

他们考虑了各种方法:

  1. 为属性设置默认值,该值适用于简单类型,但不适用于更复杂的类型。
  2. 当满足条件时使用带有break的repeat-while循环,在可读性、安全性和清晰度之间提供平衡。 承认解决这个问题类似于停止问题,并且编译器通常很难进行此类分析。
  3. 讨论 Swift 中循环表达式的可能性,类似于 Rust 或 Haskell 等函数式语言,其中循环可以“生成”一个值,帮助编译器进行必要的检查。 他们欣赏形式化循环“生成”值的想法的潜在好处,从而实现更好的编译器检查,但发现与围绕一切都是表达式构建的语言相比,针对此类功能提出的语法有点笨拙。

总之,他们寻求一种特定于 Swift 的解决方案,用于在迭代过程中初始化结构中的强制属性,并讨论在 Swift 中针对此类场景引入循环表达式的挑战和潜在好处。

  1. 讨论为什么 self 是一个强引用?

内容概括

讨论发现,通过将类转换为结构,消除分析结果中观察到的保留和释放调用,他们的 Swift 基准测试有了显着的性能改进。 他们质疑为什么这些调用在某些方法中是必要的,特别是当应保证 self 在整个方法执行过程中有效时。

回答认为 Swift 隐式 main 中的变量是全局变量,容易被重新赋值,需要额外的保留来保护。 他们提出了替代方案,例如将变量更改为常量或重组代码以使用真正的局部范围。

此外,他们还提到了对代码所做的更改,通过用 UnicodeScalar 数组替换 String 来减轻保留和释放调用,并强调了 String 由于处理字素簇而导致的复杂性以及分配、保留和释放调用的潜力。

也有人警告在性能至关重要时不要使用字符串或字符,并建议避免使用此类类型以减轻 ARC(自动引用计数)流量。 他们还建议在分析 ARC 行为时删除打印语句以排除与字符串相关的代码,尽管无需运行或分析修改后的代码。

  1. 讨论接受 Type 并返回该 Type 的实例的通用函数

问题

该问题是由于尝试在 Swift 协议函数中使用类型参数根据条件返回特定类型实例 (shadowFilter) 而引起的。 协议 ObjectRequestable 有一个方法 getObject(type: T.Type) -> T? 旨在返回特定类型的实例(如果在一致类中可用)。

然而,在 FilterManager 的实现中,尝试将 ShadowFilter 返回为 T 会导致编译器错误,因为无法将 ShadowFilter 直接转换为泛型类型 T。编译器还会标记 ShadowFilter.self 的表达式模式与泛型类型之间的不匹配。 T 型。

这里的挑战在于尝试在协议函数内有条件地返回特定类型实例,而不需要直接类型转换。

为了实现所需的功能,可能需要替代方法或不同的代码结构方式。

protocol ObjectRequestable {
    func getObject<T>(type: T.Type) -> T?
}

struct ShadowFilter {
    let width: Float
}

struct RainbowFilter {
    
}

class FilterManager: ObjectRequestable {
    
    private let shadowFilter = ShadowFilter(width: 8)
    
    func getObject<T>(type: T.Type) -> T? {
        switch type {
        case ShadowFilter.self: return shadowFilter
        default: return nil
        }
    }
}

let filterManager = FilterManager()
// Should contain the shadowFilter instance.
let shadow = filterManager.getObject(type: ShadowFilter.self)
// Should contain nil.
let rainbow = filterManager.getObject(type: RainbowFilter.self)

回答

这样写可行的:

func getObject<T>(type: T.Type) -> T? {
    if let v = shadowFilter as? T { return v }
    else { return nil }
}
  1. 讨论覆盖默认协议实现 我最近试图为协议 Foo 设置一个默认实现,它可以根据对象是否也符合另一个协议 Bar 为其属性 baz 返回不同的值。 当执行下面的操作时,结果是运行时崩溃 EXC_BAD_ACCESS。
protocol Foo {}

protocol Bar {
    var baz: Bool { get }
}

class FooBar: Foo, Bar {}

class FooBaz: Foo, Codable {}

extension Foo {
    var baz: Bool {
        return (self as? Bar)?.baz ?? false
    }
}

let fooBar = FooBar()
let fooBaz = FooBaz()

func doSomething(with foo: Foo) {
    print(foo.baz)
}

doSomething(with: fooBar)
doSomething(with: fooBaz)

回答

FooBar 符合 Foo。 Foo 有一个带有属性 baz 的扩展。 因此,FooBar包含一个名为 baz 的成员属性。

FooBar 符合 Bar。 Bar 需要名为 baz 的属性的实现。 由于 FooBar 包含一个名为 baz 的成员属性,其签名与 Bar 的要求(它从 Foo 获得的属性)相同,因此编译器选择它来满足要求。

当您获取 FooBar 实例上的属性 baz 时,它将 self 转换为 Bar,然后获取其 baz 属性。 但是,由于 Foo 的 baz 属性满足了 Bar 的 baz 属性要求,因此该属性最终会递归调用其 getter 直到堆栈溢出

  1. 讨论SSWG-0027: MongoKitten SSWG-0027:MongoKitten 的审核现已开始,持续两周,直至 2024 年 1 月 17 日。

介绍

MongoDB 是一种流行的 NoSQL 数据库管理系统,它使用面向文档的数据模型。 MongoKitten 是一个 MongoDB 客户端库,支持所有非企业功能。 它通过 BSON 的编码器和解码器支持 Codable,并且供应商专门提供基于 async/await 的 API。

动机

MongoKitten 是 Swift 生态系统的一个长期库,自 2015 年以来一直在开发。MongoDB 还创建了另一个数据库驱动程序,该驱动程序提供了包装其内部 C 实现的 Swift API。 然而,该驱动程序是生态系统中的一个相对较新的成员,自此已停产。

为 MongoDB 提供解决方案对于 Swift 生态系统至关重要,因为它服务于数据库市场的很大一部分。

建议的解决方案

MongoKitten 分为多个模块。

MongoCore 包含 MongoDB 有线协议的基本实现。

MongoClient包含一个基于该协议的MongoDB客户端。 它包含用户与 MongoDB 通信所需的所有工具。 它具有发送和接收消息以及进行身份验证的能力。 此外,MongoClient 还具有用于发送/读取消息的帮助程序,在此类连接上发送和接收符合 Codable 的消息。 最后,MongoClient 有一个 MongoDB Cluster 客户端实现,充当连接池,同时还观察集群拓扑的变化。

MongoKittenCore 是一个包含以 Codable 类型实现的最常用消息的模块。

MongoKitten 模块本身导入上述所有内容,并提供更高级别的 API 用于与 MongoDB 交互。 这是大多数用户最终与之交互的库。

最后,Meow 模块提供类似 ORM 的帮助程序,通过使您的 Codable 类型符合模型协议,可以快速存储和读取 MongoDB 中的实体。

推荐博文

swift 中的冻结枚举和非冻结枚举

摘要: 本博客探讨了在 Swift 中的冻结枚举和非冻结枚举的概念。回顾了在传统的 Objective-C 和 C 中,枚举类型是一个整数列表,并介绍了非冻结枚举和冻结枚举的概念,类比了 OC 中的 NS_ENUM 和NS_CLOSED_ENUM 。在 Swift中,用户定义的枚举基本上都是冻结枚举。

对于非冻结枚举,讨论了在使用 switch 语句时需要增加 @unknown default 来处理未来可能的case新增情况。相对于 default, @unknown default 在未列举 case 时会产生警告,而 default 不会。博客最后总结了处理非冻结枚举时的最佳实践,强调了使用 @unknown default 或 @unknown case 来做兜底处理的必要性,以避免在枚举有新增 case 时导致异常情况的发生。

手把手教你用 Swift 实现命令行工具

摘要: 这篇博客讲解了用 Swift 实现命令行工具,选择 Swift 的原因包括对 Swift 开发者友好以及 Swift 作为完全开源的语言具有更强的语言抽象能力、类型系统安全性和性能。通过一个计算器示例,教读者创建项目、引入依赖( ArgumentParser 库),以及实现加法和汇率转换功能。详细介绍了 ArgumentParser 的优点和核心逻辑,同时展示了命令行调试和发布安装的方法,最后鼓励使用Swift进行小工具开发。

使用 SwiftUI 创建康威生命游戏

摘要: 这篇博客中作者使用 SwiftUI 创建康威生命游戏(Conway's Game of Life)。Conway's Game of Life 是一款自动游戏,由一个 2D 网格组成,其中的细胞可以是活的或死的。每个细胞在下一代的状态基于其周围细胞在当前一代中的状态,遵循一些简单的规则。

文章首先,使用 SwiftUI 的 Grid 容器视图展示游戏状态并在游戏变化时进行动画处理;其次,实现根据游戏的四个规则从一代到下一代改变细胞状态的逻辑。展示了使用 Canvas 视图的不同方法,包括从 2D 数组和从模型获取数据的两种方式。

文章还介绍了使用 SwiftUI 创建康威生命游戏的不同视图,包括使用 Grid 和 Canvas 的不同布局方式。LifeModel 用于包含和控制生命游戏的核心逻辑,而 LifeViewModel 则用于在模型和视图之间进行桥接,实现数据的传递和控制。CanvasFromModelView 演示了如何使用 Canvas 视图显示来自 LifeModel 的数据。

话题讨论

过去的 2023 年你完成了哪些目标?(多选)

  1. 健康生活:保持健康饮食,维持理想的体重和健康状态。
  2. 职业发展:升职加薪,或者转换到自己满意的工作领域。
  3. 学业进步:获得更高的学历,提升专业技能。
  4. 财务自由:投资取得收益,实现财务独立和自由。
  5. 幸福家庭:建立和谐家庭关系,或者迎接新成员的到来。
  6. 旅行冒险:探索新的旅行地,或者完成一次特别的旅行。
  7. 精神成长:培养更多的兴趣爱好,深化对某领域的了解。
  8. 社交关系:建立更多深厚的友谊,拓展社交圈。
  9. 志愿者服务:参与社区服务或慈善事业。
  10. 爱情奇迹:寻找到理想的爱情关系。

欢迎在文末留言参与讨论。

关于我们

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

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

讨论在 Swift 中引入函数体宏

前言

文章内容精选

提案:函数体 Macros 提案正在审查中

Swift 论坛:Swift 中引入函数体宏

话题讨论:

你是更能接受同性上司还是更能接受异性上司?

上期话题结果

投票结果反映,大多数开发者还是比较担心自己的头发,另外就是身体变胖。久坐缺乏运动会导致一系列的身体健康问题。建议大家抽时间多运动,避免久坐。

提案

通过的提案

SE-0411 隔离的默认值表达式 提案通过审查。该提案已在 四十期周报 正在审查的提案模块做了详细介绍。

正在审查的提案

SE-0414 基于区域的隔离 提案正在审查。

Swift Concurrency 将值分配给由 actor 和任务边界确定的隔离域。在不同隔离域中运行的代码可以并发执行,并且通过 Sendable 检查,防止非 Sendable 值跨越隔离边界传递,从而彻底消除对共享可变状态的并发访问。在实践中,这是一个重要的语义限制,因为它禁止了无数据竞争的自然编程模式。

在本文档中,我们提出通过引入一种新的控制流敏感的诊断来放宽这些规则,该诊断确定非 Sendable 值是否可以安全地在隔离边界上传递。通过引入隔离区域的概念,编译器可以保守地推断两个值是否可能相互影响。通过使用隔离区域,语言可以证明在将非 Sendable 值传递过隔离边界后,该值(以及可能引用它的任何其他值)在调用者中不会被使用,从而防止竞争的发生。

SE-0415 函数体 Macros 提案正在审查。

宏通过附加代码来增强 Swift 程序,其中包括新的声明、表达式和语句。目前,宏系统不支持可能希望增强代码的关键方式之一,即合成或更新函数的主体。可以创建具有自己函数主体的新函数,但不能为用户声明的函数提供、增强或替换函数主体。

该提案引入了函数体宏,确切地说:允许根据声明全面合成函数主体,以及通过更多功能增强现有函数主体。这为宏提供了许多新的用例,包括:

  • 根据函数声明和一些元数据(例如自动合成传递提供的参数的远程过程调用)全面合成函数主体。
  • 通过执行日志/跟踪、检查前置条件或建立不变量来增强函数主体。
  • 根据提供的实现替换函数主体。例如,将主体移入在其他地方执行的闭包中,或将主体视为宏“降低”为可执行代码的特定领域语言。

Swift论坛

  1. 提议SE-0415:函数体宏

内容概括

SE-0415 提议在 Swift 中引入函数体宏。 该提案的审核期截至 2023 年 12 月 20 日。该提案建议启用一项可通过带有 -enable-experimental-feature BodyMacros 标志的主干开发快照访问的功能。

审核过程鼓励反馈以改进提案。 它要求审阅者评估所解决问题的重要性、提案是否符合 Swift 的方向,并将其与其他语言或库中的类似功能(如果适用)进行比较。 Tony Allevato 作为审核经理负责监督此审核流程。

讨论的序言中提出的一个具体问题提到,所提议的宏可能无法有效地处理从函数体抛出的错误。 有人建议使用一种新的延迟块来捕获抛出的错误,从而允许访问块内的这些错误以进行处理。

所提出的语法示例演示了一个概念,其中 defer 块可能会捕获从函数体抛出的错误并执行报告错误和重新抛出错误等操作。

  1. 讨论枚举案例类型签名/插值

内容概括

讨论围绕获取具有关联值的枚举情况的字符串表示,特别是寻求一种为每种情况生成类型化签名或插值的方法。 一个示例枚举了具有关联值及其预期输出签名的各种情况。

当前的方法涉及使用反射,但由于反射元数据对应用程序二进制大小的影响,因此存在可能删除反射元数据的担忧。 另一种考虑的方法是使用宏,但这些可能不适用于较旧的操作系统版本,从而带来兼容性挑战。

该对话强调了与处理重复的枚举案例名称相关的编译器错误,该错误使枚举实例的唯一性变得复杂。

核心需求是为某些枚举案例的所有实例导出一个稳定的 hashValue,无论它们的关联值如何,旨在将具有相同案例名称但不同关联值的实例视为相同的存储目的。 然而,传统的 Hashable 实现不足以实现此目的。

一个探索的想法涉及利用 String(describing:) 生成枚举案例的字符串表示形式,但如果客户端为枚举实现 CustomStringConvertible,则这种方法可能会失败。 人们有兴趣了解如何调用枚举描述的默认 Swift 标准库实现,以解决 CustomStringConvertible 的客户端实现所产生的问题。

  1. 讨论~Copyable 类型中不可用的 deinit

内容概括

讨论围绕着 ~Copyable 类型中不存在不可用的 deinit 以及它对程序员构建代码以与本地数据流分析保持一致的依赖展开。

不可破坏类型的概念旨在增强本地数据流分析并提供编译时保证。 它类似于函数的想法,从技术上讲,函数承诺返回一些东西,但实际上却没有,而编译器静态地证明了理论上的不可能。

该提案引入了不可破坏类型(Destructible)作为取代Copyable 的新根类型。 它设想了类型不需要显式反初始化的场景,依赖编译器的静态分析来强制执行预期的清理例程。

讨论对比了使用和不使用此功能时 API 使用的难度,强调了需要显式清理时面临的潜在挑战。 对 API 文档、运行时检查和潜在风险的仔细研究与用于防止错误使用的编译时诊断进行了比较。

对话承认实现此功能的潜在复杂性以及收益是否值得付出努力的不确定性。 它引用了 Scott Meyers 关于使接口易于正确使用且难以错误使用的名言,强调了编程接口简单性和清晰性的重要性。

  1. 展示Advent of Code 2023

内容概括

Swift 社区受邀参加“Advent of Code”,这是一项从 12 月 1 日到 12 月 25 日举办的年度编码挑战赛。 这项挑战由 Eric Wastl 组织,涉及日常编码练习,开始时很简单,逐渐变得更具挑战性。

参与者可以使用任何编程语言,但有一个 Swift 团队模板可供那些喜欢 Swift 的人使用。 该模板提供了解决挑战的起点。

加入:

  1. 克隆 Swift 团队的入门模板(可选)。
  2. 在 Advent of Code 网站上创建一个帐户(参与排行榜所需)。
  3. 使用ID 3315857-f51d8ce5加入Swift社区排行榜。

我们鼓励参与者每天使用 Swift 尝试挑战。 排行榜跟踪完成时间,但分数只是为了好玩,可以忽略。

排行榜允许那些想要分享解决方案的人链接到 GitHub 帐户,为参与者提供了互相学习方法的机会。

这是一个社区活动,并不正式隶属于 Swift 项目,旨在整个 12 月享受乐趣、提高 Swift 技能并享受一些编码挑战。 参与者被警告,随着挑战变得更加严峻,挫败感可能会出现!

  1. 讨论Swift OpenAPI Generator 1.0.0-alpha.1(候选发布版)已发布(Multipart、base64、过滤、递归类型等)

内容概括

Swift OpenAPI Generator 团队已发布版本 1.0.0-alpha.1,该版本作为即将发布的 1.0 版本的候选版本,预计将在大约两周内发布。 1.0 的主要重点是增强文档和示例,没有计划更改代码。

自 2023 年 5 月以 0.1.0 版本首次开源以来,Swift OpenAPI Generator 已经取得了实质性进展。 合并了 200 多个拉取请求,产生了 24 项更新并引入了重要的新功能。

主要亮点:

  • 新功能包括对 Base64 编码数据的支持、文档过滤、递归类型支持、服务器 URL 模板变量支持以及具有类型安全和流式传输的多部分内容类型支持。
  • 此外,生成代码的可自定义访问修饰符允许在公共、包(默认)和内部可见性之间进行选择。
  • 该版本还包含各种改进和错误修复,例如将 Swift 5.9 更新为最低支持版本、错误处理增强以及生成的代码注释的细化。

重大变更和迁移:

  • 该版本包括 API 反馈所必需的重大更改。 提供了将代码从版本 0.3.x 迁移到 1.0.0-alpha.1 的指南,详细说明了潜在的构建错误及其解决方案。

下一步是什么:

  • 版本 1.0.0-alpha.1 作为候选版本,邀请反馈意见被考虑用于计划在两周内发布的最终 1.0.0 版本。 鼓励用户测试 alpha 版本以确保与其项目的兼容性。

该团队对贡献者表示感谢,并邀请通过 Swift OpenAPI Generator GitHub 存储库进一步参与。

  1. 讨论可编码不正确的 Json 字符串,包含 URL 和十进制存在值

内容概括

该对话探讨了 Swift Codable 协议在处理存在类型时的细微差别,特别是涉及 URL、Decimal 和 AnyEncodable 的可编码行为。

讨论解决了使用存在类型时期望与实际行为之间的差异。 值得注意的是,当抽象具有预期行为(例如,meow())的 Cat 等类型的实例时,预期 Cat 的所有实例都将统一表现出该行为。 当使用encode()时,内部表示(例如Decimal类型)会出现在最终的JSON字符串中,这会让人感到惊讶,从而导致方法分派和类型编码的混乱。

该演讲深入探讨了 Codable 的基础知识以及存储类型信息以进行解码的必要性。 出于安全性和互操作性原因,可编码省略了编码数据中的类型信息,因此需要在代码中预定义以进行解码。 这种方法允许解码不明确的值,但对类型擦除的值(如 AnyEncodable)带来了挑战,使得在解码期间难以对类型进行逆向工程。 如果解码时不知道类型,则不可能重建原始数据。

所讨论的警告方面围绕着未来可能需要解码的场景。 如果在不考虑未来解码要求的情况下做出编码决策,则可能会使数据检索变得复杂。

最后,讨论暗示了从枚举案例数组中收集枚举时的挑战和注意事项,强调了编码和解码策略的复杂性以及在设计导出或序列化工具时深思熟虑的重要性。

  1. 讨论是否可以自动验证值更改并抛出错误?

内容概括

本讨论围绕自动验证值更改的概念展开,旨在消除 CRUD 方法中出现的显式验证调用。 对话的重点是在 Swift 构造中实现自动验证的挑战。

该示例使用 Name 结构来探索拦截值访问以进行实时验证的潜在方法。 然而,诸如计算属性或属性观察器之类的现有机制缺乏对在验证过程中抛出错误的直接支持。 这一限制对在 Swift 结构中无缝实现自动验证造成了重大障碍。

这次对话强调了手动验证的必要性,即使是基本类型,因为从这些基本类型构建的复杂类型会产生复杂性。 例如,讨论介绍了 Employee 结构体,并说明了对其 addr1 和 addr2 属性的手动验证规则的需求,强调尽管基本类型具有验证机制,但手动验证在复杂类型级别至关重要。

尽管函数体宏被认为是另一种潜在的方法,但讨论主要集中在计算变量或动态查找功能是否可以支持自动验证,最终表达了对在 Swift 现有机制中实现它的可行性的怀疑。

提出了两种建议的“手动”方法:

  1. 使用 let 代替 var 字段,并在构造函数中加入验证逻辑,使其失败。
  2. 在外部执行验证,如果验证失败,则利用 didSet 恢复到之前的值。

这些手动方法旨在在更改期间同步强制验证,确保值保持一致。 但是,后一种方法可能会暂时使不变量无效,但可能适用于可接受同步验证的场景,例如避免由于暂时不正确的值导致的 UI 闪烁。

话题讨论

你是更能接受同性上司还是更能接受异性上司?

能 不能

欢迎在文末留言参与讨论。

关于我们

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

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

掌握 SwiftUI 中的 ScrollView:滚动几何

前言

本文探讨了如何使用 onScrollGeometryChange 视图修饰符有效地监控和管理滚动位置和几何。通过详细的代码示例和解释,你将学习如何利用这些工具创建动态和响应迅速的用户界面。

SwiftUI 是一个强大的框架,它简化了在苹果平台上构建用户界面的过程。SwiftUI 中的一个基本组件是 ScrollView,它允许用户通过滚动导航内容。然而,管理滚动位置和理解滚动交互可能是一个挑战。ScrollGeometry 和 onScrollGeometryChange 视图修饰符的引入解决了这些挑战,为开发者提供了更多的控制和对滚动行为的深入了解。

什么是 ScrollPosition

ScrollPosition 是一种类型,允许开发者以编程方式读取或更改滚动位置。虽然有用,但当用户使用手势与滚动视图交互时,它显得不够全面。以下是一个展示 ScrollPosition 使用的示例:

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll to offset") {
                position.scrollTo(point: CGPoint(x: 0, y: 100))
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

在这个示例中,我们将滚动视图绑定到一个状态属性。当按下按钮时,滚动视图会将其内容偏移移动到指定点。然而,我们无法读取用户通过手势交互设置的具体内容偏移。

引入 ScrollGeometry

SwiftUI 的新 ScrollGeometry 类型以及 onScrollGeometryChange 视图修饰符提供了一个解决方案。这些工具允许开发者在用户交互期间准确读取内容偏移。

使用 onScrollGeometryChange

让我们探索如何使用 onScrollGeometryChange 视图修饰符与 ScrollGeometry:

struct ContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var offsetY: CGFloat = 0
    
    var body: some View {
        ScrollView {
            ForEach(1..<100, id: \.self) { number in
                Text(verbatim: number.formatted())
                    .id(number)
            }
        }
        .scrollPosition($scrollPosition)
        .onScrollGeometryChange(for: CGFloat.self) { geometry in
            geometry.contentOffset.y
        } action: { oldValue, newValue in
            if oldValue != newValue {
                offsetY = newValue
            }
        }
        .onChange(of: offsetY) {
            print(offsetY)
        }
    }
}

onScrollGeometryChange 视图修饰符接受三个参数:

  1. 类型参数:指定要跟踪的滚动几何类型。在此示例中,我们使用 CGFloat 来跟踪内容偏移的 Y 轴。
  2. 转换闭包:从 ScrollGeometry 实例中提取所需信息。
  3. 动作闭包:处理滚动几何的变化,通过比较旧值和新值,允许我们相应地更新状态属性。

高级滚动几何跟踪

ScrollGeometry 提供了许多有价值的属性,如内容偏移、边界、容器大小、可见矩形、内容插入和内容大小。开发者可以提取单个属性或组合多个属性以获得全面的见解。

以下是一个结合内容大小和可见矩形跟踪的示例:

struct ContentView: View {
    struct ScrollData: Equatable {
        let size: CGSize
        let visible: CGRect
    }
    
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var scrollData = ScrollData(size: .zero, visible: .zero)
    
    var body: some View {
        ScrollView {
            ForEach(1..<100, id: \.self) { number in
                Text(verbatim: number.formatted())
                    .id(number)
            }
        }
        .scrollPosition($scrollPosition)
        .onScrollGeometryChange(for: ScrollData.self) { geometry in
            ScrollData(size: geometry.contentSize, visible: geometry.visibleRect)
        } action: { oldValue, newValue in
            if oldValue != newValue {
                scrollData = newValue
            }
        }
        .onChange(of: scrollData) {
            print(scrollData)
        }
    }
}

在这个示例中,我们定义了一个 ScrollData 结构来保存大小和可见矩形属性。在使用 onScrollGeometryChange 视图修饰符时,我们将 ScrollData 作为转换闭包的返回类型,从 ScrollGeometry 实例中提取所有所需的数据。

完整代码示例分析

下面是一个完整的 SwiftUI Demo,其中包含了我们刚刚讨论的 ScrollView、ScrollGeometry 和 onScrollGeometryChange 的使用示例。你可以在 Xcode 中运行这个项目来观察其效果。

完整代码示例

import SwiftUI

struct ContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var offsetY: CGFloat = 0
    
    var body: some View {
        VStack {
            Text("Scroll Offset: \(offsetY, specifier: "%.2f")")
                .padding()
            
            ScrollView {
                ForEach(1..<100, id: \.self) { number in
                    Text(verbatim: number.formatted())
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color(.secondarySystemBackground))
                        .cornerRadius(8)
                        .padding(.horizontal)
                        .id(number)
                }
            }
            .scrollPosition($scrollPosition)
            .onScrollGeometryChange(for: CGFloat.self) { geometry in
                geometry.contentOffset.y
            } action: { oldValue, newValue in
                if oldValue != newValue {
                    offsetY = newValue
                }
            }
            .onChange(of: offsetY) {
                print(offsetY)
            }
        }
    }
}

struct ScrollData: Equatable {
    let size: CGSize
    let visible: CGRect
}

struct AdvancedContentView: View {
    @State private var scrollPosition = ScrollPosition(y: 0)
    @State private var scrollData = ScrollData(size: .zero, visible: .zero)
    
    var body: some View {
        VStack {
            Text("Content Size: \(scrollData.size.width, specifier: "%.2f") x \(scrollData.size.height, specifier: "%.2f")")
                .padding()
            Text("Visible Rect: \(scrollData.visible.origin.x, specifier: "%.2f"), \(scrollData.visible.origin.y, specifier: "%.2f") - \(scrollData.visible.width, specifier: "%.2f") x \(scrollData.visible.height, specifier: "%.2f")")
                .padding()
            
            ScrollView {
                ForEach(1..<100, id: \.self) { number in
                    Text(verbatim: number.formatted())
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color(.secondarySystemBackground))
                        .cornerRadius(8)
                        .padding(.horizontal)
                        .id(number)
                }
            }
            .scrollPosition($scrollPosition)
            .onScrollGeometryChange(for: ScrollData.self) { geometry in
                ScrollData(size: geometry.contentSize, visible: geometry.visibleRect)
            } action: { oldValue, newValue in
                if oldValue != newValue {
                    scrollData = newValue
                }
            }
            .onChange(of: scrollData) {
                print(scrollData)
            }
        }
    }
}

@main
struct ScrollViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                ContentView()
                    .tabItem {
                        Label("Basic", systemImage: "1.square.fill")
                    }
                
                AdvancedContentView()
                    .tabItem {
                        Label("Advanced", systemImage: "2.square.fill")
                    }
            }
        }
    }
}

如何运行

  1. 打开 Xcode 并创建一个新的 SwiftUI 项目。
  2. 将默认生成的 ContentView.swift 文件替换为上面的完整代码。
  3. @main 注释下的应用程序入口点中,确保你的主视图是 ScrollViewDemoApp
  4. 运行项目。

功能解释

  • ContentView: 展示基本的滚动偏移追踪功能,通过 onScrollGeometryChange 视图修饰符追踪 Y 轴的内容偏移。
  • AdvancedContentView: 展示更高级的滚动几何追踪功能,追踪内容大小和可见矩形的变化。
  • ScrollViewDemoApp: 包含 TabView,方便在基本和高级示例之间切换。

总结

今天,我们探讨了 SwiftUI 中的新 ScrollGeometry 类型和 onScrollGeometryChange 视图修饰符。这些工具为开发者提供了对滚动位置和交互的精确控制和洞察,增强了动态和响应迅速的用户界面的开发。通过利用这些功能,你可以创建更具吸引力和直观的应用程序。

如何使用 SwiftUI 中 ScrollView 的滚动偏移

前言

WWDC 24 已经结束,我决定开始写一些关于 SwiftUI 框架即将推出的新特性的文章。今年,苹果继续填补空白,引入了对滚动位置更细粒度的控制。本周,我们将学习如何操作和读取滚动偏移。

使用 scrollPosition

SwiftUI 框架已经允许我们通过视图标识符跟踪和设置滚动视图的位置。这种方法效果不错,但不足以更准确地跟踪用户交互。

struct ContentView: View {
    @State private var position: Int?
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(0..<100) { index in
                    Text(verbatim: index.formatted())
                        .id(index)
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $position)
    }
}

在上面的代码示例中,我们使用了视图标识符和 scrollPosition 修饰符来跟踪和设置滚动视图的位置。虽然这种方法效果不错,但在某些情况下,尤其是需要更精确的用户交互跟踪时,它可能不够用。为了弥补这一不足,SwiftUI 引入了新的 ScrollPosition 类型,使我们能够通过偏移量、滚动视图的边缘、视图标识符等组合滚动位置。

新的 ScrollPosition 类型

SwiftUI 框架引入了新的 ScrollPosition 类型,使我们能够通过偏移量、滚动视图的边缘、视图标识符等组合滚动位置。

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll to bottom") {
                position.scrollTo(edge: .bottom)
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
            
            Button("Scroll to top") {
                position.scrollTo(edge: .top)
            }
        }
        .scrollPosition($position)
    }
}

如上例所示,我们定义了 position 状态属性,并使用 scrollPosition 视图修饰符将滚动视图与状态属性绑定。我们还放置了两个按钮,允许你快速滚动到滚动视图中的第一个或最后一个项目。ScrollPosition 类型提供了许多重载的 scrollTo 函数,使我们能够处理不同的情况。

为滚动添加动画

通过附加动画视图修饰符并传递 ScrollPosition 类型的实例作为 value 参数,我们可以轻松地为编程滚动添加动画。

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll to bottom") {
                position.scrollTo(edge: .bottom)
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
            
            Button("Scroll to top") {
                position.scrollTo(edge: .top)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

滚动到特定项目

我们添加了另一个按钮来将滚动视图的位置更改为随机项目。我们仍然使用 ScrollPosition 类型的 scrollTo 函数,但我们提供了一个可哈希的标识符。这个选项允许我们将位置更改为特定项目,通过使用 anchor 参数,我们可以选择所选视图的哪个点应该可见。

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll somewhere") {
                let id = (1..<100).randomElement() ?? 0
                position.scrollTo(id: id, anchor: .center)
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

滚动到特定偏移

最后但同样重要的是 scrollTo 函数的 point 参数重载,允许我们传递 CGPoint 实例以将视图滚动到内容的特定点。

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {
            Button("Scroll to offset") {
                position.scrollTo(point: CGPoint(x: 0, y: 100))
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

如上例所示,我们使用带有 CGPoint 参数的 scrollTo 函数。它还提供重载,允许我们仅按 X 或 Y 轴滚动视图。

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    
    var body: some View {
        ScrollView {            
            Button("Scroll to offset") {
                position.scrollTo(y: 100)
                position.scrollTo(x: 200)
            }
            
            ForEach(1..<100) { index in
                Text(verbatim: index.formatted())
                    .id(index)
            }
        }
        .scrollPosition($position)
        .animation(.default, value: position)
    }
}

读取滚动位置

我们学习了如何使用新的 ScrollPosition 类型操作滚动位置,这也允许我们读取滚动视图的位置。ScrollPosition 提供了可选的 edge、point 和 viewID 属性,以在你编程滚动时读取值。

每当用户与滚动视图交互时,这些属性将变为 nil。ScrollPosition 类型上的 isPositionedByUser 属性允许我们了解何时用户手势移动滚动视图内容。

提供一个可以运行示例

下面是一个可以运行的示例代码,演示如何读取和显示滚动视图的位置。我们将使用一个 Text 视图来显示当前滚动位置:

import SwiftUI

struct ContentView: View {
    @State private var position = ScrollPosition(edge: .top)
    @State private var scrollOffset: CGPoint?

    var body: some View {
        VStack {
            ScrollView {
                LazyVStack {
                    ForEach(0..<100) { index in
                        Text("Item \(index)")
                            .id(index)
                            .padding()
                            .background(Color.yellow)
                            .cornerRadius(10)
                            .padding(.horizontal)
                    }
                }
                .scrollPosition($position)
                .onScrollGeometryChange { geometry in
                    scrollOffset = geometry?.contentBounds.origin
                }
            }
            .animation(.default, value: position)
            
            if let offset = scrollOffset {
                Text("Scroll Offset: x = \(Int(offset.x)), y = \(Int(offset.y))")
                    .padding()
            } else {
                Text("Scroll Offset: not available")
                    .padding()
            }
        }
        .padding()
    }
}

在这个示例中,我们使用了 onScrollGeometryChange 修饰符来读取滚动视图的几何变化。每当滚动视图滚动时,geometry?.contentBounds.origin 将提供当前滚动位置的偏移量。我们将这个偏移量存储在 scrollOffset 状态属性中,并在视图底部显示当前的滚动位置。

总结

在本文中,我们深入探讨了 SwiftUI 框架中 ScrollView 的新特性,特别是如何通过 ScrollPosition 类型实现更精确的滚动控制。我们介绍了如何使用 ScrollPosition 类型进行滚动位置的设置和读取,包括使用偏移量、视图标识符等方式进行操作。此外,我们还展示了如何通过动画和事件处理来增强用户体验。通过这些新功能,开发者可以更灵活地控制滚动视图的行为,从而创建更加流畅和直观的用户界面。希望这些内容对你有所帮助。

SwiftUI 在 WWDC 24 之后的新变化

前言

WWDC 24 已经到来,我们有很多内容要讨论。每年,SwiftUI 都会通过引入更多功能来赶上 UIKit。今年也不例外。让我们深入了解 SwiftUI 框架引入的新功能。

我首先要提到的主要变化是 App、Scene 和 View 协议的 @MainActor 隔离。这可能会破坏你的代码,所以请记住这一点。

视图集合

SwiftUI 为 Group 和 ForEach 视图引入了新的重载,允许我们创建自定义容器,如 List 或 TabView。

struct AppStoreView<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack {
            Group(subviewsOf: content) { subviews in
                HStack {
                    if !subviews.isEmpty {
                        subviews[0]
                    }
                    
                    if subviews.count > 1 {
                        subviews[1]
                    }
                }
                
                if subviews.count > 2 {
                    VStack {
                        subviews[2...]
                    }
                }
            }
        }
    }
}

如上例所示,我们使用带有新初始化器的 Group 视图,允许我们访问通过 @ViewBuilder 闭包传递的内容视图的子视图。SwiftUI 引入了新的 SubviewSubviewsCollection 类型,提供了对真实视图的代理访问。

新的标签栏体验

使用新的 Tab 类型,SwiftUI 提供了新的可定制标签栏体验,带有流畅过渡到侧边栏。

enum Destination: Hashable {
    case home
    case search
    case settings
    case trends
}

struct RootView: View {
    @State private var selection: Destination = .home
    
    var body: some View {
        TabView {
            Tab("home", systemImage: "home", value: .home) {
                HomeView()
            }
            
            Tab("search", systemImage: "search", value: .search) {
                SearchView()
            }
            
            TabSection("Other") {
                Tab("trends", systemImage: "trends", value: .trends) {
                    TrendsView()
                }
                Tab("settings", systemImage: "settings", value: .settings) {
                    SettingsView()
                }
            }
            .tabViewStyle(.sidebarAdaptable)
        }
    }
}

如上例所示,我们使用新的 Tab 类型来定义标签。我们还在 TabSection 实例上使用 tabViewStyle 视图修饰符,将特定的标签部分分组并移动到侧边栏。

英雄动画

SwiftUI 引入了 matchedTransitionSourcenavigationTransition,我们可以在任何 NavigationLink 实例中配对使用。

struct HeroAnimationView: View {
    @Namespace var hero
    
    var body: some View {
        NavigationStack {
            NavigationLink {
                DetailView()
                    .navigationTransition(.zoom(sourceID: "myId", in: hero))
            } label: {
                ThumbnailView()
            }
            .matchedTransitionSource(id: "myId", in: hero)
        }
    }
}

这使我们能够在 NavigationStack 内从一个视图导航到另一个视图时,使用相同的标识符和命名空间创建平滑的过渡。

滚动位置

新的 ScrollPosition 类型与 scrollPosition 视图修饰符配对,允许我们读取 ScrollView 实例的精确位置。我们还可以使用它编程地滚动到滚动内容的特定点。

struct ScrollPositionExample: View {
    @State private var position: ScrollPosition = .init(point: .zero)
    
    var body: some View {
        ScrollView {
            ForEach(1..<1000) { item in
                Text(item.formatted())
            }
            
            Button("jump to top") {
                position = ScrollPosition(point: .zero)
            }
        }
        .scrollPosition($position)
    }
}

Entry 宏

新的 Entry 宏允许我们快速引入环境值、聚焦值、容器值等,无需样板代码。让我们看看在 Entry 宏之前我们如何定义环境值。

struct ItemsPerPageKey: EnvironmentKey {
    static var defaultValue: Int = 10
}

extension EnvironmentValues {
    var itemsPerPage: Int {
        get { self[ItemsPerPageKey.self] }
        set { self[ItemsPerPageKey.self] = newValue }
    }
}

现在,我们可以通过使用 Entry 宏来简化代码。

extension EnvironmentValues {
    @Entry var itemsPerPage: Int = 10
}

预览

新的 Previewable 宏允许我们在预览中引入状态,而无需将其包装到额外的包装视图中。

#Preview("toggle") {
    @Previewable @State var toggled = true
    return Toggle("Loud Noises", isOn: $toggled)
}

其他

SwiftUI 框架的下一版本包括许多新 API,如窗口推送、TextField 和 TextEditor 视图中的文本选择观察、搜索焦点监控、自定义文本渲染、新的 MeshGradient 类型等等,我无法在一篇文章中涵盖所有内容。

总结

在 WWDC 24 上,SwiftUI 再次通过引入更多新功能来提升其成熟度,以赶上 UIKit。今年的主要变化包括 @MainActor 隔离、视图集合的新重载、新的可定制标签栏体验、英雄动画、滚动位置的新功能以及新的 Entry 和 Previewable 宏。这些改进使开发者能够创建更灵活和高效的用户界面。SwiftUI还引入了许多新的API,如窗口推送、文本选择观察、搜索焦点监控等,使开发更加便捷和强大。

使用 Swift 6 语言模式构建 Swift 包

前言

我最近了解到,Swift 6 的一些重大变更(如完整的数据隔离和数据竞争安全检查)将成为 Swift 6 语言模式的一部分,该模式将在 Swift 6 编译器中作为可选功能启用。

这意味着,当你更新 Xcode 版本或使用 Swift 6 编译器的 Swift 工具链时,除非你明确启用 Swift 6 语言模式,否则你的代码将使用 Swift 5 语言模式进行编译。

在本文中,我将向你展示如何下载和安装 Swift 6 工具链的开发快照,并在构建 Swift 包时启用 Swift 6 语言模式。

下载 Swift 6 工具链

使用 Swift 6 编译器和语言模式构建代码的第一步是下载 Swift 6 开发工具链。

Apple 在 swift.org 网站上提供了从 release/6.0 分支构建的 Swift 编译器版本,适用于多个平台,你可以下载并安装到系统中。

你可以手动执行此操作,但我建议使用像 Swiftenv(用于 macOS)或 Swiftly(用于 Linux)这样的工具来管理你的 Swift 工具链,就像本文中所示的那样。

Swiftenv - macOS

Swiftenv 是一个受 pyenv 启发的 Swift 版本管理器,它允许你轻松安装和管理多个版本的 Swift。

使用 Swiftenv,安装最新的 Swift 6 开发快照只需运行以下命令:

# 安装最新的 Swift 6 开发工具链
swiftenv install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

# 进入你的 Swift 包目录
cd your-swift-package

# 将 Swift 6 工具链设置为此目录的默认工具链
swiftenv local 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

Swiftly - Linux

如果你在 Linux 机器上构建代码,可以使用 Swift Server Workgroup 的 Swiftly 命令行工具来安装和管理 Swift 工具链,运行以下命令:

# 安装最新的 Swift 6 开发工具链
swiftly install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

# 将 Swift 6 工具链设置为活动工具链
swiftly use 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

在 SPM 中启用语言模式

让我们考虑一个 Swift 包目标,其代码在使用 Swift 6 编译器和 Swift 6 语言模式编译时会产生错误:

class NonIsolated {
    func callee() async {}
}

actor Isolated {
    let isolated = NonIsolated()
    
    func callee() async {
        await isolated.callee()
    }
}

让我们使用我们之前下载的 Swift 6 工具链并启用 StrictConcurrency 实验功能进行构建:

如你所见,构建结果是警告而不是错误。这是因为默认情况下,Swift 6 编译器使用的是 Swift 5 语言模式,而 Swift 6 语言模式是可选的。

有两种方法可以启用 Swift 6 语言模式:直接从命令行通过将 -swift-version 标志传递给 swift 编译器,或者在包清单文件中指定它。

命令行

要启用 Swift 6 语言模式编译代码,可以使用以下命令:

swift build -Xswiftc -swift-version -Xswiftc 6

包清单文件

你可以通过更新 tools-version 到 6.0 并在包清单文件中添加 swiftLanguageVersions 键来为你的 Swift 包启用 Swift 6 语言模式:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Swift6Examples",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        .library(
            name: "Swift6Examples",
            targets: ["Swift6Examples"]
        )
    ],
    targets: [
        .target(name: "Swift6Examples")
    ],
    swiftLanguageVersions: [.version("6")]
)

输出

正如你所见,当启用了 Swift 6 语言模式后,编译器报告了与数据隔离相关的错误。这些错误表明我们在代码中存在需要修复的并发问题。

结论

Swift 6 带来了许多重要的新特性,如数据隔离和数据竞争安全检查,这些特性有助于编写更安全、更高效的代码。然而,这些新特性并不会自动启用,需要通过 Swift 6 语言模式显式开启。通过下载和安装 Swift 6 工具链,并在命令行或包清单文件中启用 Swift 6 语言模式,我们可以提前体验和适应这些变化。尽管新特性带来了一些学习和调整成本,但它们最终会使我们的代码更加健壮。

如何使用 Swift 中的 GraphQL

前言

我一直在分享关于类型安全和在 Swift 中构建健壮 API 的更多内容。今天,我想继续探讨类型安全的话题,介绍 GraphQL。GraphQL 是一种用于 API 的查询语言。本周,我们将讨论 GraphQL 的好处,并学习如何在 Swift 中使用它。

基础知识

首先介绍一下 GraphQL。GraphQL 是一种用于 API 的查询语言。通常,后端开发人员或网络服务会为你提供一个模式文件和一个 GraphQL 端点。模式文件包含所有你可以使用该端点进行的类型和查询。让我们来看一个模式文件的例子。

schema {
  query: Query
  mutation: Mutation
}

type Query {
  film(id: ID, filmID: ID): Film
  allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection
  """更多代码"""
}

模式文件应包含 QueryMutation 类型。这些类型定义了当前 GraphQL 端点支持的所有查询和变更操作。模式文件还描述了你可以在查询中使用的所有类型的列表。

type Film implements Node {
  title: String!
  episodeID: Int
  openingCrawl: String
  director: String!
}

GraphQL 是一种强类型语言。GraphQL 自定义类型中的每个字段都必须声明其类型。默认情况下,每个字段都可以为 nil。带有感叹号的字段不能为 nil。

我使用星球大战 API 来向你展示本文中的示例。让我们继续进行一些查询。你可以通过 GraphiQL 应用轻松玩转 GraphQL API,使用以下端点。

query AllFilms {
  allFilms {
    films {
      title
    }
  }
}

响应:

{
  "data": {
    "allFilms": {
      "films": [
        {
          "title": "A New Hope"
        },
        {
          "title": "The Empire Strikes Back"
        },
        {
          "title": "Return of the Jedi"
        },
        {
          "title": "The Phantom Menace"
        },
        {
          "title": "Attack of the Clones"
        },
        {
          "title": "Revenge of the Sith"
        }
      ]
    }
  }
}

如你所见,我们使用模式文件中的数据类型构建我们的查询。我喜欢GraphQL的一点是响应格式。请求格式直接映射到响应格式。你可以在请求中添加更多字段,响应也会包含它们。

query AllFilms {
  allFilms {
    films {
      title
      director
    }
  }
}

响应:

{
  "data": {
    "allFilms": {
      "films": [
        {
          "title": "A New Hope",
          "director": "George Lucas"
        },
        {
          "title": "The Empire Strikes Back",
          "director": "Irvin Kershner"
        },
        {
          "title": "Return of the Jedi",
          "director": "Richard Marquand"
        },
        {
          "title": "The Phantom Menace",
          "director": "George Lucas"
        },
        {
          "title": "Attack of the Clones",
          "director": "George Lucas"
        },
        {
          "title": "Revenge of the Sith",
          "director": "George Lucas"
        }
      ]
    }
  }
}

使用 GraphQL,我们只获取我们请求的数据,绝不会多余。

ApolloGraphQL

ApolloGraphQL 是一个很棒的框架,它可以让你轻松进行 GraphQL 查询和变更。ApolloGraphQL iOS 框架负责缓存和代码生成。ApolloGraphQL 为你在项目中定义的查询和变更生成 Swift 类型。它通过自动生成所有样板代码来节省你的时间。

以下是将 ApolloGraphQL 设置到项目中的一些步骤:

  1. 你应该使用SPM或其他包管理器将 ApolloGraphQL 嵌入到你的项目中。
  2. 在编译源代码部分上方的构建阶段添加运行脚本。这个脚本下载模式并为你的查询生成 Swift 类型。你可以在这个脚本中轻松更改 GraphQL 端点以连接到你的 GraphQL 后端。

我们已准备好使用 ApolloGraphQL 的项目。现在我们可以向项目添加第一个查询。我们应该在项目中创建一个带有 .graphql 扩展名的文件,并将这些行放入文件中。

query AllFilms {
  allFilms {
    films {
      title
      director
    }
  }
}

让我们现在构建项目。ApolloGraphQL 生成一个 API.swift 文件,你应该将其添加到项目中。所有需要的类型都在这里,可以非常类型安全地进行 GraphQL 查询。每个请求类型都定义了其响应类型。ApolloGraphQL 生成了 AllFilmsQuery 和 Data 类型,描述了请求和响应。现在我们可以使用生成的代码进行 GraphQL 请求。

let url = URL(string: "https://swapi-graphql.netlify.app/.netlify/functions/index")!
let client = ApolloClient(url: url)

client.fetch(query: AllFilmsQuery()) { result in
    switch result {
    case .success(let response):
        print(response.data?.allFilms?.films ?? [])
    case .failure(let error):
        print(error)
    }
}

结论

GraphQL 为 API 开发带来了诸多优势,尤其是在类型安全和数据查询方面。通过定义明确的模式文件,GraphQL 确保了请求和响应的一致性,使得开发者能够精准获取所需数据,避免多余信息的传输。此外,GraphQL 强类型的特性进一步提升了代码的可靠性和可维护性。

在 Swift 中,ApolloGraphQL 框架极大地简化了 GraphQL 查询和变更的实现过程,自动生成的 Swift 类型和缓存机制不仅提高了开发效率,还减少了样板代码的编写。总之,GraphQL 是一种高效、灵活且类型安全的API解决方案,适用于构建现代化应用程序。尽管 GraphQL 也有其挑战,但其带来的优势使其成为 REST API 的有力竞争者。通过不断探索和优化,GraphQL 将在更多项目中得到广泛应用。

如何在 SwiftUI 视图中显示应用图标和版本

前言

在应用中显示应用图标和版本是为用户提供快速识别应用版本和变体的好方法,无论是内部用户(如测试人员或利益相关者)还是外部用户。

在本文中,我将展示如何创建一个可访问的 SwiftUI 视图,既能显示应用图标和版本,又能在各种文本大小和外观下看起来都很好:

获取应用图标

构建视图的第一步是从主包中获取应用图标。可以通过检索应用的 Info.plist 文件中的一组键值来完成,如 Stack Overflow 上的这个答案所示:

AppIconProvider.swift
import Foundation

enum AppIconProvider {
    static func appIcon(in bundle: Bundle = .main) -> String {
        guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
              let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
              let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
              let iconFileName = iconFiles.last else {
            fatalError("Could not find icons in bundle")
        }
        return iconFileName
    }
}

让我们逐步了解以上代码:

  1. 我们检索 Info.plist 文件中 CFBundleIcons 键的值。这个值是一个包含应用图标信息的嵌套字典。
  2. 我们检索 CFBundleIcons 字典中 CFBundlePrimaryIcon 键的值。这是一个包含应用主图标信息的字典。
  3. 我们检索 CFBundlePrimaryIcon 字典中 CFBundleIconFiles 键的值。这是一个包含应用图标文件名的数组。这些文件名可用于创建命名的 UIImage
  4. 最后,我们检索 CFBundleIconFiles 数组中的最后一个值。

获取应用版本

现在我们有了应用图标,让我们检索应用版本字符串。类似之前的步骤,我们需要读取应用 Info.plistCFBundleShortVersionString 键的值:

AppVersionProvider.swift
import Foundation

enum AppVersionProvider {
    static func appVersion(in bundle: Bundle = .main) -> String {
        guard let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String else {
            fatalError("CFBundleShortVersionString should not be missing from info dictionary")
        }
        return version
    }
}

如果你想在视图中包含版本号和构建号,可以检索 CFBundleVersion 键的值。

创建 SwiftUI 视图

现在让我们将所有内容结合起来,创建一个 SwiftUI 视图,显示应用图标和版本:

AppVersionInformationView.swift
import SwiftUI

struct AppVersionInformationView: View {
    let versionString: String
    let appIcon: String

    var body: some View {
        HStack(alignment: .center, spacing: 12) {
            if let image = UIImage(named: appIcon) {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            }
            VStack(alignment: .leading) {
                Text("Version")
                    .bold()
                Text("v\(versionString)")
            }
            .font(.caption)
            .foregroundColor(.primary)
        }
        .fixedSize()
        .accessibilityElement(children: .ignore)
        .accessibilityLabel("App version \(versionString)")
    }
}

让我们逐步了解以上代码:

  1. 该视图需要两个参数:应用版本和应用图标。这些值通过我们之前创建的提供者传递给视图。
  2. 我们在一个水平堆栈中显示应用图标和版本,间距为12点。
  3. 我们在 Image 视图中显示应用图标。但是,应用图标只能作为命名的 UIImage 检索,所以我们需要先创建 UIImage,然后再转换为 SwiftUI Image
  4. 我们在一个垂直堆栈中显示应用版本,包括一个标签和应用版本字符串。
  5. 我们使用 fixedSize() 修饰符确保应用图标和 VStack 视图的高度相同。
  6. 最后,我们将所有子视图组合成一个可访问的元素,并为其提供标签,以便为 VoiceOver 用户提供更好的体验。

最终结果是一个在各种文本大小下都看起来很好的视图:

在应用中显示版本信息视图

Copy code
ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        AppVersionInformationView(
            versionString: AppVersionProvider.appVersion(),
            appIcon: AppIconProvider.appIcon()
        )
    }
}

总结

在这篇文章中,我们学习了如何在 SwiftUI 应用中显示应用图标和版本信息。这对于用户(无论是内部测试人员还是外部用户)来说,是一种快速识别应用版本和变体的有效方法。我们通过从主包中检索 Info.plist 文件中的相关键值,获取应用图标。具体步骤包括读取 CFBundleIconsCFBundlePrimaryIconCFBundleIconFiles 键的值,并获取图标文件名。接着,我们采用类似的方法,通过读取 Info.plist 文件中的 CFBundleShortVersionString 键的值,获取应用版本信息。

在获取到应用图标和版本信息后,我们创建了一个 SwiftUI 视图来展示这些信息。该视图使用水平堆栈(HStack)布局,将应用图标和版本信息并排显示。同时,我们通过使用 fixedSize() 修饰符确保视图高度一致,并使用 accessibilityElement 使其对 VoiceOver 用户友好。通过这些步骤,开发者可以轻松实现一个兼具功能性和可访问性的版本信息视图。

使用 SwiftUI 为 macOS 创建类似于 App Store Connect 的选择器

前言

最近,我一直在为我的应用开发一个全新的界面,它可以让你查看 TestFlight 上所有可用的构建,并允许你将它们添加到测试群组中。

作为这项工作的一部分,我需要创建一个组件,允许用户从特定构建中添加和删除测试群组。我希望构建类似于 App Store Connect 中的选择器组件,使用户体验尽可能熟悉,并在本文中,将展示如何使用 SwiftUI 为 macOS 构建了这个组件。

创建选择器组件

让我们分析一下,我们有一组想要在 SwiftUI 列表中显示的构建。每个构建都包含一组属性,其中之一是 betaGroups,它是一个表示构建所属测试群组的结构体数组。

struct VersionBuild: Identifiable, Equatable {
    let number: String
    let date: Date
    let hasAppClip: Bool
    let iconURL: URL?
    let id: String
    let isProcessing: Bool
    var betaGroups: [BetaGroup]
}

struct BetaGroup: Identifiable, Equatable {
    let id: String
    let name: String
}

列表使用一个名为 TestFlightBuildCell 的简单组件来显示构建信息:

struct TestFlightBuildCell: View {
    let build: VersionBuild
    
    var body: some View {
        
        HStack(spacing: 12) {
            if let appIcon = build.iconURL {
                KFImage(appIcon)
                    .retry(maxCount: 3, interval: .seconds(5))
                    .cacheOriginalImage()
                    .resizable()
                    .appIconImage(size: .small)
                
            }
            
            VStack(alignment: .leading) {
                
                HStack(alignment: .center) {
                    
                    VStack(alignment:.leading){
                        
                        Text("Build \(build.number)")
                            .font(.HELheadline)
                            .foregroundStyle(.primary)
                            .place(.leading)
                        
                        
                        Text(build.date.fullText)
                            .font(.HELfootnote)
                            .foregroundStyle(.secondary)
                        
                        if build.hasAppClip {
                            Label("Includes App Clip", systemImage: "appclip")
                                .font(.HELfootnote)
                                .foregroundStyle(.secondary)
                        }
                    }
                    
                    Spacer()
                    
                    if build.isProcessing {
                        BuildTag(
                            title: "PROCESSING",
                            background: build.isProcessing ? Color.warning.opacity(0.3) : Color.gray.opacity(0.1)
                        )
                    }
                }
            }
        }
    }
}

在应用程序的上下文中,列表如下所示:

虽然上面的组件可以很好地传达所需的构建信息,但它在应用程序的这个部分仍然缺少一些关键功能。我们需要能够决定构建属于哪些测试群组,并根据需要添加或删除它们。

让我们看看 SwiftUI 中测试群组选择器组件的代码:

struct BetaGroupPicker: View {
    // 1
    @Binding var betaGroups: [BetaGroup]
    // 2
    let availableBetaGroups: [BetaGroup]
    // 3
    @State var hoveringGroup: BetaGroup?
    
    var body: some View {
        HStack(spacing: 4) {
            // 4
            ForEach(betaGroups) { betaGroup in
                Text(betaGroup.displayName)
                    .padding(4)
                    .background(Color.gray.opacity(0.2))
                    .bold()
                    .clipShape(Circle())
                    // 5
                    .onHover { hovering in
                        withAnimation {
                            hoveringGroup = hovering ? betaGroup : nil
                        }
                    }
                    // 6
                    .overlay(alignment: .topTrailing) {
                        if hoveringGroup == betaGroup {
                            Button {
                                withAnimation {
                                    betaGroups.removeAll(where: { $0 == betaGroup })
                                }
                            } label: {
                                Image(systemName: "minus.circle.fill")
                                    .foregroundStyle(Color.red)
                            }
                            .buttonStyle(.plain)
                            .offset(x: 2, y: -4)
                        }
                    }
            }
            
            // 7
            if !availableBetaGroups.isEmpty {
                Menu {
                    ForEach(availableBetaGroups) { betaGroup in
                        Button {
                            withAnimation(.snappy) {
                                betaGroups.append(betaGroup)
                            }
                        } label: {
                            Text(betaGroup.name)
                        }
                    }
                } label: {
                    Text(Image(systemName: "plus"))
                        .padding(4)
                        .background(Color.blue.opacity(0.2))
                        .bold()
                        .clipShape(Circle())
                }
                .menuStyle(.button)
                .buttonStyle(.plain)
            }
        }
    }
}

以上代码片段中涉及了很多内容,让我们来逐步分解:

  1. 绑定到构建中可用的测试群组数组。这是一个绑定,因为我们希望能够从内部视图修改它。
  2. 所有可用于添加到构建中的测试群组的数组。父视图负责提供这些信息,正如我们将在下一节中看到的那样。
  3. 一个状态属性,用于跟踪用户悬停的测试群组。此属性的值用于在用户悬停在上面时显示一个移除按钮。
  4. 遍历构建所属的测试群组,并使用 BetaGroup 结构体上的 displayName 属性将它们显示为圆形文本视图。
  5. 当用户悬停在特定测试群组组件上时,修改 hoveringGroup 状态属性。
  6. 使用 .overlay 修改器在用户悬停在测试群组组件上时显示一个移除按钮。该按钮从构建所属的测试群组列表中移除测试群组。
  7. 如果有任何可用的测试群组可以添加到构建中,则显示一个加号按钮,让用户选择要添加的测试群组。

以上代码片段使用了 BetaGroup 结构体上的一个名为 displayName 的属性来显示测试群组的名称,类似于在 App Store Connect 中的显示方式,显示名称中的前两个单词的首字母大写:

extension BetaGroup {
    var displayName: String {
        let output = name
            .components(separatedBy: .whitespaces)
            .filter { $0.lowercased() != "and" && $0.lowercased() != "&" }
            .prefix(2)
            .map { $0.first?.uppercased() ?? "" }
            .joined()
        
        return output.isEmpty ? "TF" : output
    }
}

使用选择器组件

现在我们有了 BetaGroupPicker 视图,我们可以开始在 TestFlightBuildCell 组件中使用它,让用户可以从特定构建中添加和删除测试群组:

struct TestFlightBuildCell: View {
    @Binding var build: VersionBuild
    let availableBetaGroups: [BetaGroup]

    init(
        build: Binding<VersionBuild>,
        availableBetaGroups: [BetaGroup]
    ) {
        self._build = build
        self.availableBetaGroups = availableBetaGroups.filter { !build.wrappedValue.betaGroups.contains($0) }
    }
    
    var body: some View {
        
        HStack(spacing: 12) {
            if let appIcon = build.iconURL {
                KFImage(appIcon)
                    .retry(maxCount: 3, interval: .seconds(5))
                    .cacheOriginalImage()
                    .resizable()
                    .appIconImage(size: .small)
                
            }
            
            VStack(alignment: .leading) {
                
                HStack(alignment: .center) {
                    
                    VStack(alignment:.leading){
                        
                        Text("Build \(build.number)")
                            .font(.HELheadline)
                            .foregroundStyle(.primary)
                            .place(.leading)
                        
                        
                        Text(build.date.fullText)
                            .font(.HELfootnote)
                            .foregroundStyle(.secondary)
                        
                        if build.hasAppClip {
                            Label("Includes App Clip", systemImage: "appclip")
                                .font(.HELfootnote)
                                .foregroundStyle(.secondary)
                        }
                    }
                    
                    Spacer()
                    
                    if build.isProcessing {
                        BuildTag(
                            title: "PROCESSING",
                            background: build.isProcessing ? Color.warning.opacity(0.3) : Color.gray.opacity(0.1)
                        )
                    }
                }
            }

            BetaGroupPicker(
                betaGroups: $build.betaGroups,
                availableBetaGroups: availableBetaGroups
            )
        }
    }
}

正如你所看到的,使用该组件非常简单。你只需要将父视图上的 build 属性修改为一个绑定,并将可用的测试群组传递给组件。

正如你所看到的,我们编写了一个自定义的初始化方法来过滤出任何已经属于构建的测试群组。

总结

文章介绍了如何使用 SwiftUI为macOS 创建类似于 App Store Connect 的选择器组件。作者在应用程序中添加了一个新的界面,允许用户查看 TestFlight 上所有可用的构建,并将它们添加到测试群组中。为了实现这一功能,作者创建了一个名为 BetaGroupPicker 的组件,该组件允许用户从特定构建中添加和删除测试群组。

BetaGroupPicker 中,用户可以看到构建所属的测试群组,并有选择地将它们添加到或从构建中移除。文章还提供了 TestFlightBuildCell 组件的示例,演示了如何在构建信息中集成 BetaGroupPicker 组件,以便用户可以直接在界面上操作测试群组。通过这一步骤,用户可以更方便地管理测试群组,并为应用程序的测试和部署提供更好的支持。

❌