普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-Swift社区周报

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

作者 Swift社区
2024年9月30日 11:13

前言

自 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社区
2024年9月23日 19:16

前言

本期是 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社区
2024年9月20日 19:21

前言

本期是 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社区
2024年9月17日 23:20

前言

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

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

作者 Swift社区
2024年9月6日 12:14

摘要

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

引言

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

调试工具的使用

断点调试

断点调试是最基本的调试方式之一,它允许开发者在代码执行过程中暂时中止程序,并逐步检查变量、表达式和函数的状态。在大多数现代 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 包的内容

作者 Swift社区
2024年8月6日 20:47

介绍

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 中的函数式核心与命令式外壳:单向数据流

作者 Swift社区
2024年8月3日 23:25

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 过程中实施高效的自动化测试和部署

作者 Swift社区
2024年8月1日 20:41

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 中符号图像的外观

作者 Swift社区
2024年7月31日 19:38

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 的使用:滚动可见性

作者 Swift社区
2024年7月24日 21:07

前言

我们的滚动 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社区
2024年7月13日 23:42

前言

本期是 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 中引入函数体宏

作者 Swift社区
2024年7月9日 20:06

前言

文章内容精选

提案:函数体 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:滚动几何

作者 Swift社区
2024年7月8日 19:17

前言

本文探讨了如何使用 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 的滚动偏移

作者 Swift社区
2024年6月28日 20:01

前言

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 之后的新变化

作者 Swift社区
2024年6月24日 23:26

前言

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社区
2024年6月4日 20:21

前言

我最近了解到,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社区
2024年6月3日 23:32

前言

我一直在分享关于类型安全和在 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 视图中显示应用图标和版本

作者 Swift社区
2024年5月24日 17:47

前言

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

在本文中,我将展示如何创建一个可访问的 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 的选择器

作者 Swift社区
2024年5月17日 17:55

前言

最近,我一直在为我的应用开发一个全新的界面,它可以让你查看 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 组件,以便用户可以直接在界面上操作测试群组。通过这一步骤,用户可以更方便地管理测试群组,并为应用程序的测试和部署提供更好的支持。

SwiftUI 中的内容边距

作者 Swift社区
2024年4月28日 19:31

前言

SwiftUI 引入了一组视图修饰符,使我们能够有效地管理视图中的安全区域。在许多情况下,安全区域是你希望放置内容的地方。今天,我们将了解 SwiftUI 引入的新内容边距概念以及它与安全区域的区别。

创建示例

让我们从一个简单的示例开始,演示带有一百个项目的列表。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                ForEach(1..<100) { index in
                    Text("Item \(index)")
                }
            }
            .font(.title)
            .navigationTitle("项目列表")
        }
    }
}

如上例所示,我们将列表视图与一堆文本视图放在一起。在 iPhone 上可能看起来很好,但是在 iPad 上,它看起来非常奇怪,因为它将所有文本放在了前导边缘,并保持屏幕中央为空白。

在使用 UIKit 时,我们可以访问 readableContentGuide 布局指南。从字面上看,它是另一个安全区域,适应屏幕大小,但仅适用于文本内容。不幸的是,我们在 SwiftUI 中无法访问 readableContentGuide。

适配 iPad

我们可以通过增加 iPad 上的安全区域来解决此问题,如下所示:

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(1..<100) { index in
                    Text("Item \(index)")
                }
            }
            .font(.title)
            .navigationTitle("项目列表")
            .safeAreaPadding(.horizontal, sizeClass == .regular ? 200 : 0)
        }
    }
}

我们通过使用 horizontalSizeClass 环境值和 safeAreaPadding 视图修饰符,将内容移动到了 iPad 上的中心。然而,正如你所见,这也将滚动条指示器从后导边缘移到了中心。

使用 contentMargins

我们需要一种区分视图的内容和工具栏,并仅移动内容而保持工具栏在原地的方法。幸运的是,SwiftUI 引入了新的 contentMargins 视图修饰符,使我们能够在视图中移动特定类型的内容。

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(1..<100) { index in
                    Text("Item \(index)")
                }
            }
            .font(.title)
            .navigationTitle("项目列表")
            .contentMargins(
                .horizontal,
                sizeClass == .regular ? 200 : 0,
                for: .scrollContent
            )
        }
    }
}

如上例所示,我们使用 contentMargins 视图修饰符仅将可滚动内容从安全区域移开。但是它将滚动条保留在视图的后导边缘。

contentMargins 视图修饰符接受几个参数,允许我们调整其行为。第一个参数是我们想要移动的边缘。它可以是 leadingtrailingtophorizontalvertical 或一次性移动所有边缘。第二个参数是我们想要移动的空间量。第三个参数是 ContentMarginPlacement 类型的实例,它允许我们指定我们想要移动的位置。例如,它可以是 scrollContent,正如我们在示例中所做的那样。另一个选项是 scrollIndicators,它仅移动指示器。

可运行 Demo

提供一个基于提供的代码片段的简化版本的Swift Playground示例,用于演示如何使用contentMargins视图修饰符来管理内容边距。以下是示例代码:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    
    var body: some View {
        NavigationView {
            List {
                ForEach(1..<20) { index in
                    Text("Item \(index)")
                }
            }
            .font(.title)
            .navigationTitle("Item list")
            .contentMargins(
                .horizontal,
                sizeClass == .regular ? 200 : 0,
                for: .scrollContent
            )
        }
    }
}

let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController

在这个示例中,我们创建了一个简单的列表视图,其中包含 20 个项目。根据水平尺寸类别的不同(正常或紧凑),我们使用 contentMargins 视图修饰符来管理水平方向上的内容边距。在紧凑水平尺寸类别下,我们将内容移动了 200 个点,以便在大屏幕设备上居中显示。你可以在 Playground 中运行此代码以查看结果。

总结

本文介绍了 SwiftUI 中的内容边距管理,通过对比安全区域的概念,解释了内容边距的重要性。文章从创建示例开始,展示了在列表视图中如何处理内容边距的问题。随后,通过介绍 UIKit 中的 readableContentGuide 布局指南以及 SwiftUI 中的 safeAreaPadding 视图修饰符,展示了在 iPad 上适配内容边距的方法。最后,引入了 contentMargins 视图修饰符,并详细解释了其用法和参数,以及如何使用它来管理内容边距。通过本文,读者可以更好地理解并掌握 SwiftUI 中内容边距的管理技巧。

AnyView 对 SwiftUI 性能的影响

作者 Swift社区
2024年3月28日 17:37

前言

AnyView 是一种类型擦除的视图,对于 SwiftUI 容器中包含的异构视图非常方便。在这些情况下,你不需要指定视图层次结构中所有视图的具体类型。通过这种方式,你可以避免使用泛型,从而简化你的代码。

然而,这可能会带来性能损失。如果是 AnyView(基本上是一个包装类型),SwiftUI 将很难确定视图的身份和结构,并且它将重新绘制整个视图,这并不是真正高效的。你可以在这个出色的 WWDC 演讲中找到有关 SwiftUI 差异机制的更多细节。

Apple 也多次提到,我们应该避免在 ForEach 中使用 AnyView,称其可能会导致性能问题。一个可能发生的情况是无尽的不同视图列表,呈现不同类型的数据(例如聊天、活动动态等)。在本文中,我将使用 Stream 的 SwiftUI 聊天 SDK 进行一些测量,使用其默认的基于泛型的实现,并将其与使用 AnyView 的修改后的实现进行比较。

测试设置

关于测试设置的几点说明:

  • 所有测试和测量都在 iPhone 11 Pro Max 上进行。
  • 为保持一致性,在所有测试中都使用相同的数据集和用户。
  • 测试会执行多次。
  • 正在测试的列表具有不同类型的数据(例如图像、视频、GIF、文本等)。
  • 在测试不同实现时执行相同的操作(例如,在内容上滚动三次)。
  • 数据以每页 25 个项目的形式获取。
  • 我们将使用动画卡顿仪器配置文件以及这个开源 FPS 计数器。

动画卡顿仪器配置文件

动画卡顿

苹果建议使用动画卡顿作为衡量应用性能的指标。卡顿基本上是指在屏幕上显示的帧比预期晚的帧。卡顿时间越长,出现的故障和挂起就越明显,从而造成用户体验不佳。例如,如果你有 100 毫秒的卡顿,这意味着此帧显示晚于预期的 100 毫秒,从而使用户可以看到挂起。卡顿可以出现在提交阶段或渲染阶段。

为了提高我们应用的性能,我们需要将这些动画卡顿降到最低(或者更好地摆脱它们)。

我还将展示与 FPS(每秒帧数)的比较,因为它通常是开发人员更熟悉的度量标准之一。当使用 FPS 作为度量标准时,重要的是指定最大帧速率(在这种情况下为 60),并在应用程序没有活动时丢弃值。

浏览数据

首先,让我们看看在浏览内容时不同的实现会表现如何。在这个测试中,我们将通过整个消息列表三次滚动。

没有 AnyView

下面是没有泛型实现的动画卡顿记录。

如你所见,有几个动画卡顿,其中 2 个是橙色的,这意味着卡顿持续时间超过了可接受的延迟时间 33 毫秒。因此,在这 2 种情况下,将会丢失一帧。这 2 个卡顿发生在加载新消息并将其附加到消息列表时。在加载消息时进行任何后续滚动,不会影响性能。

在此测试期间,FPS 值的平均值约为每秒 59 帧。滚动是流畅且响应迅速的。

有 AnyView

接下来,让我们做同样的测试,同时使用 AnyView 包装器。以下是动画卡顿仪器配置文件中的结果。

你可以在此示例中看到一些更多的橙色。有更多的动画卡顿超过了可接受的延迟时间 33 毫秒。这导致在执行测试时在仪器和视觉上都出现一些可见的卡顿。

此外,当你再次浏览列表时,性能不会改善(甚至变得更糟)。这是有道理的,因为 SwiftUI 不知道它已经显示过此视图一次(因为它隐藏在 AnyView 下)。因此,它会再次绘制它,同时还可能缓存(但不使用)该视图的旧版本。

此测试中的平均 FPS 约为每秒 55 帧,你可能会注意到在滚动时出现一些可见的故障,尽管情况并不那么糟糕。

在浏览数据时修改

我们可以进行的另一个测试是性能测试 - 向列表发送大量内容并强制更新视图(例如,响应消息),同时我们也浏览数据。这将在较短的时间间隔内触发视图的多次重绘。

没有 AnyView

在没有 AnyView 包装器的情况下进行测试产生了与常规滚动测试相似的结果(58-59 FPS)。这也是预期的,因为 SwiftUI 知道视图的标识和结构。当需要更新视图时,仅对其进行更改(例如,向视图添加另一个反应)。

有 AnyView

当我们在这种情况下使用 AnyView 时,事情就变得有趣了 - 在短时间内对屏幕上的视图进行频繁更新。

在此场景中,有几个可见的卡顿和挂起,当我们频繁响应消息时,FPS 降至 50 以下。由于在几秒钟内强制重绘视图多次,帧丢失在这里更加明显。由于 SwiftUI 不知道这个视图是什么,我假设它每次都会从头开始重绘。其中一些视图相当昂贵(例如 GIF),因此重新绘制可能是一项相当昂贵的操作。

通过使用 AnyView,效果类似于将 id 修饰符的值设置为 UUID() - 这将在发生更改时始终更新视图项目。

分析结果

测试/实现 没有 AnyView(FPS) 有 AnyView(FPS) 性能退化
浏览数据 59 55 10%
在浏览数据时修改 59 50 16.5%

这些数字相当依赖于设置,因此不应该被视为铁板钉钉的结果,而只是一个指示。

仅浏览数据时,如果你将视图包装在 AnyView 中,则会比不包装时慢大约 10%。如果你在浏览数据时更改数据,则此差异将增加到约 17%,而且这些故障在这里更加明显。

为了更好地理解结果,我们需要深入了解 SwiftUI 的工作原理。在这个关于 SwiftUI 性能的 WWDC 会话中,来自 SwiftUI 团队的 Raj 讨论了列表或表需要提前知道所有标识符。只有在内容解析为恒定数量的行时,才能高效地收集它们而无需访问所有内容。如果使用条件检查或 AnyView,将无法确定行数,并且必须提前创建所有视图,这会影响性能。

因此,请尽量避免这样的代码:

ForEach(someData) { someElement in
  if someCondition {
    SomeView(data: someElement)
  }
}

以及像这样的代码:

ForEach(someData) { someElement in
    AnyView(SomeView(data: someElement))
}

最后一段代码类似于我们使用 AnyView 进行测试的方式。这意味着,当列表发生更改时,我们实际上重新创建了整个列表。这也解释了为什么 AnyView 实现随着时间的推移变慢 - 每次重绘时都需要从头开始创建更多内容。

总结

总而言之,在这些情景中(包含异构视图的可滚动列表),最好为容器中的不同视图使用具体类型。这可能听起来更复杂一些,但实际上你可以使其更简单,而不必过多地处理泛型。

然而,这并不意味着使用 AnyView 总是会以这种方式影响性能。例如,如果你有一个菜单,作为几个异构元素的列表,在点击时显示不同的导航目标,并且决定将这些视图包装为 AnyView,我的测量结果表明与使用其他方法相比,性能没有区别。

在这篇文章中,使用 AnyView 与使用 if-else 语句的不同类型的测试显示出没有显着差异。使用 if-else 导致视图标识丢失,就像 AnyView 一样,因此在这里没有性能差异是可以预期的。

这也取决于实现的方式 - 你的数据模型,将状态传递到哪里,哪些更新可能会导致视图重绘等等。

Swift Core Data 分阶段迁移

作者 Swift社区
2024年3月26日 19:24

前言

在这之前,我发布了一篇文章,在其中解释了如何使用映射模型和自定义迁移策略执行复杂的 Core Data 迁移。虽然这种方法性能良好且运行良好,但很难维护,不适用于应用程序扩展,并且存在高度的错误风险。

例如,对于每个需要自定义迁移的新模型,你需要定义一个映射模型,以定义如何将每个模型的现有版本迁移到新版本。与你可能认为的相反(以及我所认为的),Core Data 在跨多个版本进行迁移时并不会按顺序迭代映射模型,相反,它需要从当前版本到新版本的精确模型。

除此之外,你需要使用 Xcode 的 UI 和映射模型来定义所有这些内容,这使得 PR 难以审查,错误难以发现。出于这些原因,我最近重新设计了我们的迁移流程,改用分阶段迁移,对开发者体验产生了巨大的影响!

什么是分阶段迁移?

正如在 WWDC23 中宣布的那样,与在 Swift 数据模型之间执行迁移的方式非常相似,你现在可以使用 NSStagedMigrationManager 实例以编程方式定义 Core Data 迁移。

该方法通过定义一系列迁移步骤(称为阶段),描述了如何在模型的不同版本之间进行迁移。

例如,假设你的应用程序当前正在使用数据模型的第 1 版,你想要迁移到第 3 版。迁移管理器将顺序应用所有必要的阶段,以从第 1 版迁移到第 2 版,然后从第 2 版迁移到第 3 版。

提供一些背景信息

为了演示 Core Data 分阶段迁移的工作原理,我将使用我之前在有关使用映射模型进行自定义 Core Data 迁移的文章中使用的相同示例。

与之前的文章一样,我们想要将 Track 模型中的 json 属性转换为一个单独的实体,该实体将为每个曲目保存所有相关的艺术家信息。将此属性转换也将使模型更灵活、更易于维护,因为我们将能够删除 json 属性本身和 artistName,而使用新的关系。

让我们比较一下我们的 Track 模型之前和之后的情况,CoreData.swift 文件代码如下:

Copy code
CoreData.swift
// Before
import Foundation
import CoreData

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var json: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artistName: String?
}

// After

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artists: NSSet?

    @objc(addArtistsObject:)
    @NSManaged public func addToArtists(_ value: Artist)

    @objc(removeArtistsObject:)
    @NSManaged public func removeFromArtists(_ value: Artist)

    @objc(addArtists:)
    @NSManaged public func addToArtists(_ values: NSSet)

    @objc(removeArtists:)
    @NSManaged public func removeFromArtists(_ values: NSSet)
}

@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
        return NSFetchRequest<Artist>(entityName: "Artist")
    }

    @NSManaged public var name: String?
    @NSManaged public var id: String?
    @NSManaged public var imageURL: String?
    @NSManaged public var tracks: NSSet?

    @objc(addTracksObject:)
    @NSManaged public func addToTracks(_ value: Track)

    @objc(removeTracksObject:)
    @NSManaged public func removeFromTracks(_ value: Track)

    @objc(addTracks:)
    @NSManaged public func addToTracks(_ values: NSSet)

    @objc(removeTracks:)
    @NSManaged public func removeFromTracks(_ values: NSSet)
}

从上面的代码中可以看出,迁移并不是微不足道的,而且,对我们来说,Core Data 不能自动推断它。让我们看看如何使用分阶段迁移以代码形式定义迁移步骤。

创建迁移管理器

要定义我们的阶段,我们需要将我们的模型拆分为三个不同的模型版本和迁移:

  1. 保持原始模型版本不变。
  2. 第二个模型版本包含所有属性,并添加 Artist 实体和关系。这将是一个自定义阶段。
  3. 第三个模型版本删除了 jsonartistName 属性。这将是一个轻量级的阶段。

我们需要将迁移分解为三个阶段的原因是,就目前而言,我们不能在同一个阶段中使用并删除属性。

让我们从创建一个负责创建 NSStagedMigrationManager 实例并定义所有阶段的工厂类开始。StagedMigrationFactory.swift 文件代码如下:

import Foundation
import CoreData
import OSLog

// 1
extension Logger {
    private static var subsystem = "dev.polpiella.CustomMigration"
    
    static let storage = Logger(subsystem: subsystem, category: "Storage")
}

// 2
extension NSManagedObjectModelReference {
    convenience init(in database: URL, modelName: String) {
        let modelURL = database.appending(component: "\(modelName).mom")
        guard let model = NSManagedObjectModel(contentsOf: modelURL) else { fatalError() }
        
        self.init(model: model, versionChecksum: model.versionChecksum)
    }
}

// 3
final class StagedMigrationFactory {
    private let databaseURL: URL
    private let jsonDecoder: JSONDecoder
    private let logger: Logger
    
    init?(
        bundle: Bundle = .main,
        jsonDecoder: JSONDecoder = JSONDecoder(),
        logger: Logger = .storage
    ) {
        // 4
        guard let databaseURL = bundle.url(forResource: "CustomMigration", withExtension: "momd") else { return nil }
        self.databaseURL = databaseURL
        self.jsonDecoder = jsonDecoder
        self.logger = logger
    }
    
    // 5
    func create() -> NSStagedMigrationManager {
        let allStages = [
            v1toV2(),
            v2toV3()
        ]
        
        return NSStagedMigrationManager(allStages)
    }

    // 6
    private func v1toV2() -> NSCustomMigrationStage {
        struct Song: Decodable {
            let artists: [Artist]
            
            struct Artist: Decodable {
                let id: String
                let name: String
                let imageURL: String
            }
        }
        
        // 7
        let customMigrationStage = NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2")
        )
        
        // 8
        customMigrationStage.didMigrateHandler = { migrationManager, currentStage in
            guard let container = migrationManager.container else {
                return
            }
            
            // 9
            let context = container.newBackgroundContext()
            context.performAndWait {
                let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Track")
                fetchRequest.predicate = NSPredicate(format: "json != nil")
                
                do {
                    let allTracks = try context.fetch(fetchRequest)
                    let addedArtists = [String: NSManagedObject]()
                    for track in allTracks {
                        if let jsonString = track.value(forKey: "json") as? String {
                            let jsonData = Data(jsonString.utf8)
                            let object = try? self.jsonDecoder.decode(Song.self, from: jsonData)
                            let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
                                if let matchedArtist = addedArtists[jsonArtist.id] {
                                    return matchedArtist
                                }
                                let artist = NSEntityDescription
                                    .insertNewObject(
                                        forEntityName: "Artist",
                                        into: context
                                    )
                                
                                artist.setValue(jsonArtist.name, forKey: "name")
                                artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
                                artist.setValue(jsonArtist.id, forKey: "id")
                                
                                return artist
                            } ?? []
                            
                            track.setValue(Set<NSManagedObject>(artists), forKey: "artists")
                        }
                    }
                    try context.save()
                } catch {
                    logger.error("\(error.localizedDescription)")
                }
            }
        }
        
        return customMigrationStage
    }
    
    // 10
    private func v2toV3() -> NSCustomMigrationStage {
        NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 3")
        )
    }
}

回到上面的代码,让我们逐步分解:

  1. 我们定义了一个自定义记录器,以将迁移过程中发生的任何错误报告到控制台。
  2. 我们扩展了 NSManagedObjectModelReference,创建了一个方便的初始化方法,它接受数据库 URL 和模型名称,并返回一个新的 NSManagedObjectModelReference 实例。
  3. 我们定义了一个工厂类,负责创建 NSStagedMigrationManager 实例并定义所有阶段。
  4. 我们使用 bundle 初始化工厂,并检索数据库的 URL、JSON 解码器和记录器。
  5. 我们创建了 NSStagedMigrationManager 实例,并定义了所有阶段。
  6. 我们定义了一个方法,该方法将返回从我们模型的第 1 版迁移到第 2 版的迁移阶段。
  7. 我们创建了一个 NSCustomMigrationStage 实例,并传递我们要从何处迁移和迁移到的对象模型引用。文件名需要与包中的 .mom 文件的名称匹配。
  8. 我们定义了 didMigrateHandler 闭包,在模型迁移后调用。此时,新的模型版本可在上下文中使用,你可以填充其属性。你必须知道,还有一个在先前模型版本上执行的单独处理程序,称为 willMigrateHandler,但我们在这种情况下不会使用它。
  9. 我们创建了一个新的后台上下文,并获取所有具有 json 属性的曲目。然后,我们将 JSON 字符串解码为 Song 对象,并为 JSON 中的每个艺术家创建一个新的 Artist 实体。然后,我们将 Track 实体的 artists 关系设置为新的 Artist 实体。
  10. 我们定义了一个方法,该方法将返回从我们模型的第 2 版迁移到第 3 版的迁移阶段。这个迁移非常简单,事实上,它应该是一个轻量级的迁移。然而,我找不到一个能够在所有情况下使用的 NSLightweightMigrationStage 实例的方法。如果你知道如何做,请告诉我!

设置使用 Core Data 栈。

设置使用分阶段迁移的 Core Data 栈。

现在我们有了创建 NSStagedMigrationManager 实例的方法,我们需要设置我们的 Core Data 栈以使用它。PersistenceController.swift 文件代码如下:

PersistenceController.swift
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CustomMigration")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        if let description = container.persistentStoreDescriptions.first {
            if let migrationFactory = StagedMigrationFactory() {
                description.setOption(migrationFactory.create(), forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
            }
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

这部分非常简单,你只需要将 NSStagedMigrationManager 实例设置为持久化存储描述的选项。

总结

这篇文章介绍了使用分阶段迁移来改进 Core Data 迁移流程的重要性和方法。传统的迁移方法使用映射模型,但这种方法不易维护,扩展性差且容易出错。分阶段迁移通过定义一系列迁移步骤,使得在不同模型版本之间进行迁移变得更加简单和可控。文章以一个示例来说明分阶段迁移的工作原理,以及如何以代码形式定义迁移步骤。最后,文章展示了如何设置使用分阶段迁移的 Core Data 栈。通过使用分阶段迁移,可以显著提高开发者体验,简化迁移流程,并降低错误风险。

Swift 定制 Core Data 迁移

作者 Swift社区
2024年3月25日 19:22

前言

随着应用程序和用户群的增长,你需要添加新功能,删除其他功能,并改变应用程序的工作方式。这是软件开发生命周期的自然结果,我们应该接受。

随着应用程序的发展,你的数据模型也会发生变化。你需要更改数据结构的方式,以适应新功能,同时确保用户不会在不同版本之间丢失任何数据。如果你使用 Core Data 在应用程序中持久化信息,那么 Core Data 迁移就会发挥作用。

什么是 Core Data 迁移?

Core Data 迁移是将数据模型从一个版本更新到另一个版本的过程,因为数据的形状发生了变化(例如,添加或删除新属性)。

在大多数情况下,Core Data 将自动处理迁移过程。但是,有些情况下,你需要通过提供一个映射模型来自定义迁移过程,告诉 Core Data 究竟如何从源模型迁移到目标模型中的每个属性和实体。

甚至有些情况下,映射模型是不够的,你需要编写自定义迁移策略来处理特定情况。这是本文要重点讨论的情况。

示例

让我们考虑一个应用程序,在 Core Data 栈中存储表示音乐曲目的对象。模型非常简单,只包含一个实体:Track,Track.swift 代码如下:

Copy code
Track.swift
import Foundation
import CoreData

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var json: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artistName: String?
}

上面的 Track 实体有五个属性:

  • imageURL:表示曲目封面图像的 URL 的字符串。
  • json:表示来自服务器的原始 JSON 数据响应的字符串。
  • lastPlayedAt:表示上次播放曲目的日期。
  • title:表示曲目的标题的字符串。
  • artistName:表示艺术家的名称的字符串。

Core Data 栈不会与 iCloud 同步,并具有以下设置,CoreDataStack.swift 文件代码如下:

Copy code
CoreDataStack.swift
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CustomMigration")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        if let description = container.persistentStoreDescriptions.first {
            description.shouldMigrateStoreAutomatically = true
            description.shouldInferMappingModelAutomatically = false
        }

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

如果你仔细观察上面的示例,你会注意到我们告诉 Core Data 自动迁移存储,因为我们不想做渐进式迁移,这种迁移速度慢得多且更复杂,并且我们还告诉 Core Data 不要自动推断映射模型,这意味着我们将不得不为每个迁移提供一个映射模型文件,并且可以允许我们自定义这个过程。

持久化了一首歌曲后,使用 Core Data Lab 检查数据库,我们可以看到属性被相应保存:

更新模型

当前版本的模型存在一些可扩展性问题:

  1. 模型仅允许每个曲目有一个艺术家,而实际上,一个曲目可以有多个艺术家。
  2. 模型存储一个表示曲目数据的原始 JSON 字符串,这不太高效,当应用程序需要解析 JSON 字符串以显示曲目数据以获取艺术家列表时,可能会导致性能问题。

为了解决这些问题,让我们删除 artistNamejson 属性,采用一个新的 Artist 实体,该实体将与 Track 实体建立一对多的关系。

Artist 实体将具有一个表示艺术家名称的 name 属性,以及 idimageURL 属性,我们将从原始 JSON 字符串中获取它们。

创建一个新的模型版本

首先,让我们通过选择 .xcdatamodeld 文件,然后从菜单栏中选择 Editor > Add Model Version... 来创建一个新的模型版本。

给它起一个名称,并以第一个模型版本为基础:

现在,让我们创建 Artist 实体并添加所有字段:

也让我们为新的 Artist 实体创建 NSManagedObject 子类,Artist.swift 代码如下:

Copy code
import Foundation
import CoreData

@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
        return NSFetchRequest<Artist>(entityName: "Artist")
    }

    @NSManaged public var name: String?
    @NSManaged public var id: String?
    @NSManaged public var imageURL: String?
    @NSManaged public var tracks: NSSet?

    @objc(addTracksObject:)
    @NSManaged public func addToTracks(_ value: Track)

    @objc(removeTracksObject:)
    @NSManaged public func removeFromTracks(_ value: Track)

    @objc(addTracks:)
    @NSManaged public func addToTracks(_ values: NSSet)

    @objc(removeTracks:)
    @NSManaged public func removeFromTracks(_ values: NSSet)
}

正如你在上面的示例中看到的那样,我们将向 Track 实体添加一个对多的 artists 关系,还将向 Artist 实体添加一个对多的 tracks 关系。

现在,让我们为 Track 实体添加缺失的关系,并删除 artistNamejson 属性:

并更新 NSManagedObject 子类以反映更改,Track.swift 文件代码如下:

import Foundation
import CoreData

@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }

    @NSManaged public var imageURL: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artists: NSSet?

    @objc(addArtistsObject:)
    @NSManaged public func addToArtists(_ value: Artist)

    @objc(removeArtistsObject:)
    @NSManaged public func removeFromArtists(_ value: Artist)

    @objc(addArtists:)
    @NSManaged public func addToArtists(_ values: NSSet)

    @objc(removeArtists:)
    @NSManaged public func removeFromArtists(_ values: NSSet)
}

最后但并非最不重要的,让我们将新的模型设置为 .xcdatamodeld 文件的当前模型:

创建映射模型

由于我们告诉 Core Data 不要自动推断映射模型,所以我们将不得不创建一个映射模型文件来在两个版本之间建立桥梁。

从菜单栏中选择 File > New > File...,然后选择 Mapping Model

然后,选择源模型:

最后,选择目标模型:

编写自定义迁移策略

默认情况下,Core Data 将尽力映射属性,并且大部分工作都将由它自动完成(包括已删除的属性)。

然而,由于我们创建了一个新的实体,并且我们希望保留现有数据,因此我们需要告诉 Core Data 如何迁移。

我们将创建一个新的类,该类继承自 NSEntityMigrationPolicy,并在旧的 Track 实体上创建并链接一个新的关系到 Artist 实体,V2MigrationPolicy.swift 文件代码如下:

Copy code
import CoreData

struct Song: Decodable {
    let artists: [Artist]

    struct Artist: Decodable {
        let id: String
        let name: String
        let imageURL: String
    }
}

class V2MigrationPolicy: NSEntityMigrationPolicy {
    private let decoder = JSONDecoder()

    override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
        // 1
        let sourceKeys = sInstance.entity.attributesByName.keys
        let sourceValues = sInstance.dictionaryWithValues(forKeys: sourceKeys.map { $0 as String })

        // 2
        let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
        let destinationKeys = destinationInstance.entity.attributesByName.keys.map { $0 as String }

        // 3
        for key in destinationKeys {
            if let value = sourceValues[key] {
                destinationInstance.setValue(value, forKey: key)
            }
        }

        if let jsonString = sInstance.value(forKey: "json") as? String {
            // 3
            let jsonData = Data(jsonString.utf8)
            let object = try? decoder.decode(Song.self, from: jsonData)
            // 4
            let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
                // 5
                let request = Artist.fetchRequest()
                request.fetchLimit = 1
                request.predicate = NSPredicate(format: "name == %@", jsonArtist.name)
                // Do not add duplicates to the list...
                if let matchedArtists = try? manager.destinationContext.fetch(request), let matchedArtist = matchedArtists.first {
                    return matchedArtist
                }
                // 6
                let artist = NSEntityDescription.insertNewObject(forEntityName: "Artist", into: manager.destinationContext)

                artist.setValue(jsonArtist.name, forKey: "name")
                artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
                artist.setValue(jsonArtist.id, forKey: "id")

                return artist
            } ?? []

            // 7
            destinationInstance.setValue(Set<NSManagedObject>(artists), forKey: "artists")
        }

        // 8
        manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping)
    }
}

让我们逐步解释上面的代码:

  1. 获取源实体的属性名称和值。
  2. 创建与源实体相同类型的全新目标实体。
  3. 将源实体的属性值复制到目标实体。
  4. 如果源实体具有 json 属性,则将其解析为 Song 对象。
  5. 为避免重复项,请检查艺术家是否已经存在于目标上下文中。
  6. 如果艺术家不存在,则创建一个新的 Artist 实体,将其插入到上下文中,并设置其属性。
  7. 设置目标实体上的新艺术家关系。
  8. 将源和目标实例关联起来。

最后,让我们将此自定义策略添加到映射模型中:

现在,如果我们再次运行应用程序并使用 Core Data Lab 检查数据库,我们可以看到一个新的实体已经填充了正确的数据。

总结

文章介绍了在应用程序发展过程中,数据模型可能需要进行更改的情况下,如何使用 Core Data 迁移来保持数据的一致性和完整性。首先,它解释了什么是 Core Data 迁移,以及为什么需要进行迁移。接着,通过一个示例应用程序,详细介绍了如何更新数据模型,添加新实体和关系,以解决现有模型的可扩展性问题。然后,文章介绍了如何创建映射模型来定义不同模型版本之间的映射关系,并演示了如何编写自定义迁移策略来处理特定情况,例如将旧模型数据迁移到新模型的新关系中。最后,通过将自定义迁移策略添加到映射模型中,完成了整个迁移过程。

如何使用 SwiftUI 构建 visionOS 应用

作者 Swift社区
2024年3月22日 23:07

前言

Apple Vision Pro 即将推出,现在是看看 SwiftUI API 的完美时机,这使我们能够将我们的应用程序适应 visionOS 提供的沉浸式世界。苹果表示,构建应用程序的最佳方式是使用 Swift 和 SwiftUI。下面,我们将学习如何使用 SwiftUI 构建 visionOS 应用程序。

Windows

我喜欢 SwiftUI 的一点是它如何自动适应平台。你无需执行任何操作即可在 visionOS 上运行使用 SwiftUI 编写的应用程序。它可以即插即用。但是,你始终可以通过向前移动并适应平台功能来改进用户体验。

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List {
            // 列表内容
            }
            .navigationTitle("Models")
            .toolbar {
                ToolbarItem(placement: .bottomOrnament) {
                    Button("open", systemImage: "doc.badge.plus") {
                        
                    }
                }
                
                ToolbarItem(placement: .bottomOrnament) {
                    Button("open", systemImage: "link.badge.plus") {
                        
                    }
                }
            }
        } detail: {
            Text("Choose something from the sidebar")
        }
    }
}

在上面的示例中,我们使用了称为 bottomOrnament 的新工具栏放置。 visionOS 中的装饰是位于窗口外部的位置,用于呈现与窗口连接的控件。你还可以通过使用新的 ornament 视图修改器手动创建它们。

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List {
            // 列表内容
            }
            .navigationTitle("Models")
            .ornament(attachmentAnchor: .scene(.leading)) {
                // 在此处放置你的视图
            }
        } detail: {
            Text("Choose something from the sidebar")
        }
    }
}

新的 ornament 视图修改器允许我们为其连接的窗口创建一个具有特定锚点的装饰。将你的应用内容适应 visionOS 提供的沉浸式体验的另一种方法是使用 transform3DEffectrotation3DEffect 视图修改器来加入深度效果。如下图:

Volumes

你的应用程序可以在 visionOS 上的同一场景中并排显示 2D 和 3D 内容。在这种情况下,我们可以使用 RealityKit 框架来呈现 3D 内容。例如,RealityKit 为我们提供了 Model3D SwiftUI 视图,允许我们从 USDZ 或实际文件中显示 3D 模型。

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List(Model.all) { model in
                NavigationLink {
                    Model3D(named: model.name)
                } label: {
                    Text(verbatim: model.name)
                }
            }
            .navigationTitle("Models")
        } detail: {
            Model3D(named: "robot")
        }
    }
}

Model3D 视图的工作方式类似于 AsyncImage 视图,并异步加载模型。你还可以使用 Model3D 初始化器的另一种变体,它允许你自定义模型配置并添加占位视图。

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List(Model.all) { model in
                NavigationLink {
                    Model3D(
                        url: Bundle.main.url(
                            forResource: model.name,
                            withExtension: "usdz"
                        )!
                    ) { resolved in
                        resolved
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                    } placeholder: {
                        ProgressView()
                    }
                } label: {
                    Text(verbatim: model.name)
                }
            }
            .navigationTitle("Models")
        } detail: {
            Model3D(named: "robot")
        }
    }
}

在你的应用程序中呈现 3D 内容时,你可以使用 windowStyle 修饰符来启用内容的体积显示。体积样式允许你的内容在第三维中增长,以匹配模型的大小。

对于更复杂的 3D 场景,我们可以使用 RealityView 并填充它以 3D 内容。

struct ContentView: View {
    var body: some View {
        NavigationSplitView {
            List(Model.all) { model in
                NavigationLink {
                    RealityView { content in
                        // load the content and add to the scene
                    }
                } label: {
                    Text(verbatim: model.name)
                }
            }
            .navigationTitle("Models")
        } detail: {
            Text("Choose something from the sidebar")
        }
    }
}

沉浸式空间

visionOS 的第三个选项是完全沉浸式体验,允许我们通过隐藏周围的所有内容来专注于你的场景。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        ImmersiveSpace(id: "solar-system") {
            SolarSystemView()
        }
    }
}

正如你在上面的示例中所看到的,我们通过使用 ImmersiveSpace 类型来定义场景。它允许我们通过使用 openImmersiveSpace 环境值来启用它。

struct MyMenuView: View {
    @Environment(\.openImmersiveSpace) private var openImmersiveSpace
    
    var body: some View {
        Button("Enjoy immersive space") {
            Task {
                await openImmersiveSpace(id: "solar-system")
            }
        }
    }
}

我们还可以使用 dismissImmersiveSpace 环境值来关闭沉浸式空间。请记住,你一次只能显示一个沉浸式空间。

struct SolarSystemView: View {
    @Environment(\.dismissImmersiveSpace) private var dismiss
    
    var body: some View {
        // Immersive experience
        
        Button("Dismiss") {
            Task {
                await dismiss()
            }
        }
    }
}

结论

在介绍了 SwiftUI 在 visionOS 上的应用之后,我们了解到 SwiftUI 可以帮助我们轻松构建适应 visionOS 的应用程序。不仅如此,SwiftUI 还提供了许多方便的工具和修饰符,例如 windowStyle 修饰符,可用于在应用程序中呈现 3D 内容,并使内容根据模型的大小自动适应。通过引入沉浸式空间,我们可以将用户带入全新的体验,让他们沉浸在应用程序的世界中。总的来说,SwiftUI 为构建 visionOS 应用程序提供了强大而灵活的工具,我们可以期待在这个全新的平台上开发出令人惊叹的应用体验。

Swift 周报 第四十三期

作者 Swift社区
2024年1月15日 20:28

前言

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

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

命是弱者的借口,运是强者的谦辞,辉煌与否?且看Swift社区强中意!

周报精选

新闻和社区:iPhone 破发促销、印度市场寻增量,苹果再攀 3 万亿美元高点

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

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

推荐博文:在 SwiftUI 中实战使用 MapKit API

话题讨论:

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

上期话题结果

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

新闻和社区

关于 App Store 提交的隐私更新

2023 年 12 月 7 日,第三方 SDK 隐私清单和签名。 第三方软件开发工具包 (SDK) 能够为 App 提供强大的功能,同时也可能会影响用户隐私,而这些影响可能对开发者和用户来说并不明显。请注意,当你将第三方 SDK 与你的 App 搭配使用时,你需要对 App 中使用的相应 SDK 包含的所有代码负责,并且需要了解 SDK 的数据收集和使用实践。

在 WWDC23 (简体中文字幕) 上,我们宣布了新的 SDK 隐私清单和签名,以帮助 App 开发者更好地了解第三方 SDK 如何使用数据、保护软件依赖项并为用户提供额外的隐私保护。从 2024 年春季开始,如果你提交的新 App 或 App 更新添加了 App Store 上的 App 中常用的第三方 SDK,那么你需要包含相应 SDK 的隐私清单。将 SDK 用作二进制文件依赖项时,也需要包含签名。此功能对于所有 App 来说都是向前迈出的重要一步,我们鼓励所有 SDK 采用这项功能,以更好地支持依赖于相应 SDK 的 App。

需要声明原因的 API 的新用例。 如果你上传到 App Store Connect 的新 App 或 App 更新使用了需要声明原因的 API (包括第三方 SDK 使用的 API),而你没有在 App 的隐私清单中提供批准的原因,那么你会收到通知。根据我们收到的开发者反馈,批准的原因列表已扩展到包含更多用例。如果你的用例可让用户直接受益,但未在现有批准原因列表中,请提交请求 (英文) 以便我们添加新的原因。

从 2024 年春季开始,若要将新 App 或 App 更新上传到 App Store Connect,你需要在 App 的隐私清单中注明批准的原因,以准确反映你的 App 如何使用相应 API。

iPhone 破发促销、印度市场寻增量,苹果再攀 3 万亿美元高点

作为全球科技公司的标杆,苹果公司的市值在今年 8 月初创下了新高,突破了 3 万亿美元的大关。不过,由于手机、PC 等市场的波动,加上外部多种因素的影响,近几月的时间,苹果公司的股价起起伏伏,市场也都在等待苹果何时能够再次站上 3 万亿美元的高点。

四个月的等待后,这一节点被定格在了美国当地时间 12 月 5 日。截至美股当日收盘,苹果公司股价报收于 193.42 美元,上涨 2.11% ,总市值达 3.01 万亿美元。

临近 2023 年年底,苹果公司股价 3 万亿美元的再次冲关,也算是给过去起伏的一年收了个尾。回望过去的一年,作为贡献出近一半收入的产品,iPhone 也未能幸免于整个大环境的下滑。为了提振销量,渠道商不得不降价促销,而新品开售破发加速、华为 5G 的回归更是给了苹果重重一击。

不过,依然需要指出的是,在绝大部分手机品牌亏本做买卖的同时,苹果公司则是赚走了全球超 8 成的利润。另外,印度市场的崛起,也让苹果公司找到了新增量。

Counterpoint 高级分析师 Ivan Lam 对钛媒体 App 表示,“印度俨然已经成为了人口第一大国,而且出生率还不错,年轻群体庞大且消费活跃。对于苹果来说,是未来十年的一个重要潜力市场。”

假日将至,请为你的 App 做好准备

App Store 最繁忙的季节即将到来!确保及时更新你的 App 和游戏,并在岁末假日到来之前做好准备。整个假日季期间同样会开放 App 提交,我们非常期待看到你提交的 App。平均而言,90% 的提交内容会在 24 小时内得到审核。但请注意,在 12 月 22 日至 27 日,完成审核所需的时间可能略长一些。

提案

通过的提案

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 与 Objective-C 及混编机制

摘要: 这篇博客讨论了 Objective-C 的预编译工作机制和与 Xcode 相关的技术细节。Clang Module 提升了编译的健壮性和扩展性,而使用 hmap 技术可以提高编译效率。

Xcode Phases 构建系统中的不同类型代表不同的文件。使用 cocoapods-hmap-built 插件可以节省大型项目的编译时间。Clang Module 和 Swift Module 有相似的概念,而 Swift 与 Objective-C 混编有几种方法可选。利用 VFS 机制可以提升编译效率。

在 SwiftUI 中实战使用 MapKit API

摘要: 这篇 Swift 博客介绍了在 SwiftUI 中使用 MapKit 的基础知识。最新版本的 SwiftUI 引入了新的 MapKit 集成 API ,提供了更全面的功能。

文章示例了如何使用 Marker 和 Annotation 类型在地图上放置标记和自定义视图。还介绍了控制地图初始位置和交互类型的方法。

该博客将在接下来的几周继续深入讨论相机操作、地图样式和用户位置跟踪等主题。

计算机编程语言原理与源码实例讲解:Swift 函数和闭包

摘要: 这篇 Swift 博客介绍了计算机编程语言原理与源码实例中的 Swift 函数和闭包。文章首先介绍了 Swift 作为一种强类型、编译型、面向对象的编程语言的背景。

然后,详细讲解了函数和闭包的核心概念和联系,包括函数的定义、调用和返回值,以及闭包的定义、调用和返回值。

接下来,文章深入探讨了函数和闭包的算法原理,包括函数的接收输入参数、执行操作和返回输出结果的过程,以及闭包的类似过程。

最后,文章通过具体的代码实例展示了函数和闭包的使用方法,并讨论了它们未来的发展趋势和可能面临的挑战。附录部分回答了一些常见问题,帮助读者更好地理解 Swift 函数和闭包的概念和用法。

话题讨论

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

  1. 性别无关:不在意上司的性别,更关注他们的能力和领导风格。
  2. 同性上司:同性上司更容易理解自己的处境和需求。
  3. 异性上司:异性上司会带来不同的观点和经验。
  4. 不确定:没有明确的偏好,根据情况判断是否接受同性或异性上司。

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

关于我们

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

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

如何在 SwiftUI 中熟练使用 sensoryFeedback 修饰符

作者 Swift社区
2023年12月29日 20:18

前言

SwiftUI 引入了新的 sensoryFeedback 视图修饰符,使我们能够在所有 Apple 平台上播放触觉反馈。下面我们将学习如何使用 sensoryFeedback 修饰符在应用程序中的不同操作中提供触觉反馈。

背景介绍

在 iOS 17 之前,如果你想要从 SwiftUI 视图中向用户提供触觉反馈,你会使用其中一个 UIKit(或 AppKit)的反馈生成器。例如,使用选择反馈生成器:

struct ListView: View {
  @Binding var store: Store
  let generator = UISelectionFeedbackGenerator()
  
  var body: some View {
    List(store.items, selection: $store.selection) { ... }
    .onChange(of: store.selection) { oldValue, newValue in
      if newValue != nil {
        generator.selectionChanged()
      }
    }
  }
}

在 iOS 17 中,Apple 直接向 SwiftUI 中添加了一系列感觉反馈的视图修饰符,以播放触觉和/或音频反馈。

平台支持

并非所有平台都支持所有反馈选项。以下是我所知道的每个平台上可用的内容列表。请注意,iPad不支持触觉反馈。

仅支持watchOS

  • start:活动开始
  • stop:活动停止

支持watchOS和iOS

  • decrease:重要值减少到显着阈值以下
  • increase:重要值增加到显着阈值以上
  • selection:UI元素的值正在更改
  • success:任务成功完成
  • warning:任务产生警告
  • error:任务产生错误
  • impact:UI元素碰撞时的物理冲击

请注意,impact反馈有两个变体,让您指定元素碰撞的重量(轻,中,重)或灵活性(刚性,柔软,实心)。在这两种情况下,您还可以更改强度(默认为1.0):

// 默认impact反馈
.impact()

// 具有柔韧性并增加强度的impact
.impact(flexibility: .rigid, intensity: 2.0)

// 具有重量并增加强度的impact
.impact(weight: .heavy, intensity: 2.0)

基本用法

要在 SwiftUI 视图中播放触觉反馈,我们只需要使用 sensoryFeedback 视图修饰符,带有两个参数。第一个定义了反馈样式,第二个是触发器值。

struct ContentView: View {
    @State private var store = Store()
    
    var body: some View {
        NavigationStack {
            List(store.results, id: \.self) { result in
                Text(result)
            }
            .searchable(text: $store.query)
            .sensoryFeedback(.success, trigger: store.results)
        }
    }
}

在上面的示例中,我们使用 sensoryFeedback 视图修饰符,带有成功样式。我们还将存储的 results 属性定义为触发器。这意味着 SwiftUI 将在存储的结果更改时播放成功样式的触觉反馈。

预定义样式

SwiftUI 提供了许多预定义的反馈样式,如 successwarningerrorselectionincreasedecreasestartstopalignmentlevelChangeimpact 等。

struct ContentView: View {
    @State private var trigger = false
    
    var body: some View {
        NavigationStack {
            Button("Action") {
                // 进行某些操作
                trigger.toggle()
            }
            .sensoryFeedback(
                .impact(weight: .heavy, intensity: 0.9),
                trigger: trigger
            )
        }
    }
}

如上所示,impact 样式允许我们调整反馈的权重和强度。请记住,最好使用预定义的样式,并在超级自定义的情况下自定义触觉反馈。

根据触发器值选择样式

sensoryFeedback 视图修饰符的另一种变体允许我们根据触发器值选择特定的反馈样式。在这里,我们在存储包含结果时播放成功反馈,并在结果为空时播放错误反馈。

struct ContentView: View {
    @State private var store = Store()
    
    var body: some View {
        NavigationStack {
            List(store.results, id: \.self) { result in
                Text(result)
            }
            .searchable(text: $store.query)
            .sensoryFeedback(trigger: store.results) { oldValue, newValue in
                return newValue.isEmpty ? .error : .success
            }
        }
    }
}

SwiftUI 还提供了在触发器值上定义条件的选项,决定是否播放预定义的反馈样式。

使用场景

这些感觉反馈修饰符都是基于触发器的。触发器需要是可等同的类型。有三种感觉反馈视图修饰符的变体:

当值更改时触发

struct ListView: View {
  @Binding var store: Store
    
  var body: some View {
    List(store.items, selection: $store.selection) { ... }
    .sensoryFeedback(.selection, trigger: store.selection)
  }
}

视图修饰符的第一个参数是 SensoryFeedback 类型。并非所有反馈类型都适用于所有平台。当触发器值更改时,反馈会播放。

使用条件闭包触发

如果要更灵活地控制何时触发反馈,请使用带有条件闭包版本的视图修饰符。例如,仅在选择更改为非空值时播放选择反馈:

.sensoryFeedback(.selection, trigger: store.selection) {
  oldValue, newValue in
    newValue != nil
}

条件闭包接收监视的触发器值的旧值和新值。在闭包中,返回一个布尔值,指示是否应播放反馈。

使用反馈闭包触发

要控制播放何种反馈,请使用视图修饰符的反馈闭包版本。例如,基于错误代码提供警告或错误反馈:

// @State private var errorCode: Int = 0

.sensoryFeedback(trigger: errorCode) { oldValue, newValue in
    switch newValue {
    case 1: .warning
    case 2: .error
    default: nil
    }
}

在这种情况下,在闭包中返回所需的反馈,如果不想要任何反馈,则返回nil。

可以运行 Demo

提供一个可以运行的 Demo,完整代码如下:

import SwiftUI

struct ContentView: View {
    @State private var store = Store()
    
    var body: some View {
        NavigationView {
            List(store.results, id: \.self) { result in
                Text(result)
            }
            .searchable(text: $store.query)
            .sensoryFeedback(.success, trigger: store.results)
            .navigationTitle("Sensory Feedback Demo")
        }
    }
}

struct Store {
    @State var query: String = ""
    @State var results: [String] = ["Result 1", "Result 2", "Result 3"]
}

Demo 包括一个带有触觉反馈的 SwiftUI 列表。你可以根据需要进行进一步的调整和扩展。

总结

SwiftUI引入了新的sensoryFeedback视图修饰符,为所有Apple平台提供触觉反馈。通过简单的附加,我们可以定义反馈样式和触发器值,实现了在应用程序中不同操作产生的触觉效果。支持多种预定义样式,如success、warning、error,以及个性化的impact样式。对于触发器值的处理也非常灵活,可以根据其条件选择不同的反馈样式。

总体而言,这个新的视图修饰符为提高应用的可访问性和用户体验提供了简便的方式。在使用时需谨慎,避免过多干扰用户。希望通过学习这个新特性,开发者能更好地运用触觉反馈功能,提升应用的交互性。

在 SwiftUI 中实战应用 ContentUnavailableView

作者 Swift社区
2023年12月28日 19:15

前言

SwiftUI 引入了新的 ContentUnavailableView 类型,允许我们在应用程序中展示空状态、错误状态或任何其他内容不可用的状态。本周,我们将学习如何使用 ContentUnavailableView 引导用户浏览应用程序中的空状态。

基本用法

让我们从展示 ContentUnavailableView 视图的基本用法开始。

struct ContentView: View {
    let store: Store
    
    var body: some View {
        NavigationStack {
            List(store.products, id: \.self) { product in
                Text(verbatim: product)
            }
            .navigationTitle("Products")
            .overlay {
                if store.products.isEmpty {
                    ContentUnavailableView(
                        "Connection issue",
                        systemImage: "circle"
                    )
                }
            }
        }
    }
}

在上面的示例中,我们将 ContentUnavailableView 定义为产品列表的叠加层。每当产品列表为空时,我们使用带有标题和图像的 ContentUnavailableView 显示。ContentUnavailableView 的另一种变体还允许我们定义当前状态的描述文本。

自定义视图

struct ContentView: View {
    let store: Store
    
    var body: some View {
        NavigationStack {
            List(store.products, id: \.self) { product in
                Text(verbatim: product)
            }
            .navigationTitle("Products")
            .overlay {
                if store.products.isEmpty {
                    ContentUnavailableView {
                        Label("Connection issue", systemImage: "wifi.slash")
                    } description: {
                        Text("Check your internet connection")
                    } actions: {
                        Button("Refresh") {
                            store.fetch()
                        }
                    }
                }
            }
        }
    }
}

ContentUnavailableView 还允许我们在描述文本下方显示操作按钮。因此,ContentUnavailableView 初始化程序的另一种变体允许我们使用 ViewBuilder 闭包定义视图的每个部分,从而完全自定义其外观和感觉。

搜索屏幕使用

struct ContentView: View {
    @Bindable var store: Store
    
    var body: some View {
        NavigationStack {
            List(store.products, id: \.self) { product in
                Text(verbatim: product)
            }
            .navigationTitle("Products")
            .overlay {
                if store.products.isEmpty {
                    ContentUnavailableView.search
                }
            }
            .searchable(text: $store.query)
        }
    }
}

在搜索屏幕显示搜索结果时,可以使用 ContentUnavailableView 类型的搜索功能。它由框架本地化,并遍历视图层次结构以找到搜索栏并提取其文本以显示在视图内。

手动提供查询

struct ContentView: View {
    @Bindable var store: Store
    
    var body: some View {
        NavigationStack {
            List(store.products, id: \.self) { product in
                Text(verbatim: product)
            }
            .navigationTitle("Products")
            .overlay {
                if store.products.isEmpty {
                    ContentUnavailableView.search(text: store.query)
                }
            }
            .searchable(text: $store.query)
        }
    }
}

你还可以通过使用 ContentUnavailableView 类型的搜索功能并提供单个参数来手动将查询输入描述中。

可运行 Demo

完整可以运行的 Demo 需要有相关的环境和依赖项,而代码片段中涉及到了一些 Store 和其他可能的模型或服务。由于代码片段中的 Store 类型未提供,我将使用一个简化版本的示例代码来创建一个简单的 SwiftUI Demo,以展示 ContentUnavailableView 的基本使用。

import SwiftUI

struct Product: Identifiable {
    let id: UUID
    let name: String
}

class ProductStore: ObservableObject {
    @Published var products: [Product] = []

    func fetchProducts() {
        // Simulating product fetching
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.products = [Product(id: UUID(), name: "iPhone"), Product(id: UUID(), name: "iPad")]
        }
    }
}

struct ContentView: View {
    @StateObject var store = ProductStore()

    var body: some View {
        NavigationView {
            List(store.products) { product in
                Text(product.name)
            }
            .navigationTitle("Products")
            .overlay {
                if store.products.isEmpty {
                    ContentUnavailableView(
                        "No Products",
                        systemImage: "exclamationmark.triangle"
                    )
                }
            }
            .onAppear {
                store.fetchProducts()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

上述代码中,我们创建了一个简单的 Product 结构体表示产品,以及一个 ProductStore 类作为存储产品的模拟服务。在 ContentView 中,我们使用 ContentUnavailableView 来处理产品为空的情况。

请确保在 Xcode 中创建一个新的 SwiftUI 项目,并将上述代码替换到主 ContentView 中,然后运行该项目。在项目的初始加载时,ContentUnavailableView 将显示“No Products”消息,几秒后模拟产品加载,之后产品列表将显示在主视图中。

总结

今天,我们学习了如何在 SwiftUI 中使用 ContentUnavailableView 类型以用户友好的方式显示空状态。通过这些简单而强大的功能,我们能够更好地引导用户,使他们能够理解应用程序的当前状态。 ContentUnavailableView 的灵活性和易用性为我们处理应用程序中的不可用状态提供了有力的工具。

如何在 SwiftUI 中熟练使用 visualEffect 修饰符

作者 Swift社区
2023年12月27日 18:25

前言

在 WWDC 23 中,SwiftUI 引入了一个名为 visualEffect 的新视图修饰符。此修饰符允许我们通过访问特定视图的布局信息来附加一组可动画化的视觉效果。下面我们将学习如何在 SwiftUI 中使用新的 visualEffect 视图修饰符。

介绍 visualEffect

让我们从使用 visualEffect 视图修饰符的最简单示例开始。

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
            .visualEffect { initial, geometry in
                initial.offset(geometry.size)
            }
    }
}

正如你在上面的示例中所看到的,我们定义了一个文本视图并附加了 visualEffect 视图修饰符。每当你附加 visualEffect 视图修饰符时,你应该指定效果闭包。这是你应用所有需要的效果的地方。

效果闭包为你提供了两个参数。第一个是附加到视图的效果集合的初始状态。它是 EmptyVisualEffect 类型的实例。我们使用此实例来附加额外的效果。第二个参数是包含视图的所有布局信息的 GeometryProxy 类型的实例,比如 frame、安全区域等。

什么是视觉效果?

视觉效果是可以改变视图的视觉外观但不影响其布局的任何东西。在 SwiftUI 框架的先前版本中,我们有视图修饰符,如缩放、偏移、模糊、对比度、饱和度、不透明度、旋转等。它们全部都是视觉效果,并且现在符合 VisualEffect 协议。你可以在 visualEffect 闭包中使用其中任何一个。

struct ContentView: View {
    
    var body: some View {
        Text("Hello World!")
            .visualEffect { initial, geometry in
                initial
                    .blur(radius: 8)
                    .opacity(0.9)
                    .scaleEffect(.init(width: 2, height: 2))
            }
    }
}

像 frame 和 padding 这样的东西不是视觉效果,你不能在 visualEffect 闭包中使用它们,因为它们修改了视图层次结构的布局。

visualEffect 修饰符视觉效果

visualEffect 视图修饰符是完成旧事物的新方法。我们可以使用旧视图修饰符修改视图的不透明度和偏移。如果你不需要布局信息,你可以继续使用它们。新方法的唯一区别是我们通过从 GeometryProxy 提供的布局信息计算视图的视觉效果的方式来限定视图的视觉效果。

visualEffect 视图修饰符支持可动画化的值。因此,你可以继续使用它根据视图在视图层次结构中的框架和边界来动画化视图的视觉外观。

struct ContentView: View {
    @State private var isScaled = false
    
    var body: some View {
        VStack {
            Button("Scale") {
                isScaled.toggle()
            }
            
            Text("Hello World!")
                .visualEffect { initial, geometry in
                    initial.scaleEffect(
                        CGSize(
                            width: isScaled ? 2 : 1,
                            height: isScaled ? 2 : 1
                        )
                    )
                }
                .animation(.smooth, value: isScaled)
        }
    }
}

完整的代码

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello World!")
            .visualEffect { initial, geometry in
                initial.offset(geometry.size)
            }
    }
}

struct ContentViewWithEffects: View {
    var body: some View {
        Text("Hello World!")
            .visualEffect { initial, geometry in
                initial
                    .blur(radius: 8)
                    .opacity(0.9)
                    .scaleEffect(.init(width: 2, height: 2))
            }
    }
}

struct ContentViewWithAnimation: View {
    @State private var isScaled = false
    
    var body: some View {
        VStack {
            Button("Scale") {
                isScaled.toggle()
            }
            
            Text("Hello World!")
                .visualEffect { initial, geometry in
                    initial.scaleEffect(
                        CGSize(
                            width: isScaled ? 2 : 1,
                            height: isScaled ? 2 : 1
                        )
                    )
                }
                .animation(.smooth, value: isScaled)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
        ContentViewWithEffects()
        ContentViewWithAnimation()
    }
}

将上述代码放入 Swift 文件中,然后在 Xcode 中打开并运行,选择合适的模拟器。请注意,由于视觉效果和动画效果,最好在模拟器上查看效果。

总结

本文章介绍了在 SwiftUI 中引入的新视图修饰符 visualEffect。该修饰符允许我们通过访问特定视图的布局信息来附加一组可动画的视觉效果。给出了一些使用 visualEffect 的简单示例,包括如何使用效果闭包以及如何应用一些常见的视觉效果(例如模糊、透明度、缩放)。

此外,还提到了 GeometryProxy 类型的使用,以及 visualEffect 对可动画值的支持,使得可以根据视图的帧和边界来动态调整视觉外观。

最后,指出了 visualEffect 修饰符在向后兼容性方面的注意事项,并建议在不需要布局信息的情况下继续使用传统的视图修饰符。

如何在 SwiftUI 中开发定制 MapKit 功能

作者 Swift社区
2023年12月20日 17:26

介绍

在上一篇文章中,我们探讨了 SwiftUI 中新的 MapKit API 的基础知识。现在,让我们深入 MapKit API 的定制点,以便根据我们的需求定制地图呈现。

地图样式

新的 MapKit API 引入了 mapStyle 视图修饰符,使我们能够自定义地图上呈现的数据样式。

struct ContentView: View {
    var body: some View {
        Map {
            // ...
        }
        .mapStyle(.imagery(elevation: .realistic))
    }
}

在上面的示例中,我们使用了 mapStyle 视图修饰符,并使用了 imagery 样式和逼真的高程。imagery 样式的高程参数的另一个选项是 flat。

imagery-map

SwiftUI 为我们提供了一套预定义且可配置的地图样式。在前面的示例中,我们使用了一个称为 imagery 的样式。默认情况下,SwiftUI 框架使用标准样式。标准样式允许我们配置地图的高程、要包括或排除的兴趣点,以及是否需要显示交通信息。

struct ContentView: View {
    var body: some View {
        Map {
            // ...
        }
        .mapStyle(
            .standard(
                elevation: .flat,
                pointsOfInterest: .excluding([.store]),
                showsTraffic: false
            )
        )
    }
}

另一个选项是混合样式,允许在地图上显示影像、道路和道路名称。混合样式还配置了高程、交通和兴趣点。

struct ContentView: View {
    var body: some View {
        Map {
            // ...
        }
        .mapStyle(
            .hybrid(
                elevation: .flat,
                pointsOfInterest: .including([.airport]),
                showsTraffic: true
            )
        )
    }
}

地图交互

MapKit 支持与地图的不同类型交互,包括缩放、平移、倾斜和旋转地图上的内容。默认情况下,SwiftUI 激活所有可用手势,但你可以轻松将可用交互限制为首选交互的列表。

struct ContentView: View {
    var body: some View {
        Map(interactionModes: [.pan, .pitch]) {
            // ...
        }
    }
}

地图控件

每当将 MapKit 与 SwiftUI 一起导入时,你将获得可用作地图控件的特定 SwiftUI 视图。这些视图包括 MapScaleViewMapCompassMapPitchToggleMapUserLocationButtonMapZoomStepper 视图。

struct ContentView: View {
    var body: some View {
        Map {
            // ...
        }
        .mapControls {
            MapScaleView()
            MapCompass()
        }
    }
}

你可以将这些视图与 mapControls 视图修饰符一起使用,为在 SwiftUI 视图层次结构中共享相同环境的任何地图实例指定控件。当你将 MapScaleViewMapCompass 视图放在 mapControls 视图修饰符内时,SwiftUI 会处理控件的放置,具体取决于运行应用的平台。

这些地图控件是简单的 SwiftUI 视图,这意味着你可以在 mapControls 视图修饰符之外的任何位置使用它们。在这种情况下,要将地图控件绑定到特定的地图实例,你应该使用 mapScope 视图修饰符。

struct MapScopeExample: View {
    @Namespace private var favoritesMap
    var body: some View {
        VStack {
            Map(scope: favoritesMap) {
                // 收藏的标记
            }
            
            HStack {
                MapScaleView(scope: favoritesMap)
                MapCompass(scope: favoritesMap)
            }
        }
        .mapScope(favoritesMap)
    }
}

如上例所示,我们使用 Namespace 属性包装器生成一个地图标识符,将控件绑定到地图实例。当你需要更改自动可见性配置为始终可见或隐藏时,还可以使用 mapControlVisibility 视图修饰符。

struct MapScopeExample: View {
    @Namespace private var favoritesMap
    var body: some View {
        VStack {
            Map(scope: favoritesMap) {
                // 收藏的标记
            }
            
            HStack {
                MapScaleView(scope: favoritesMap)
                MapCompass(scope: favoritesMap)
                    .mapControlVisibility(.hidden)
            }
        }
        .mapScope(favoritesMap)
    }
}

总结

本文介绍了 SwiftUI 中 MapKit API 的定制功能。首先,通过 mapStyle 视图修饰符,我们学习了如何定制地图的呈现样式,包括 imagery 样式的高程设置。其次,我们了解了预定义和可配置的地图样式,例如 standard 样式允许配置地图的高程、感兴趣点和是否显示交通信息,而 hybrid 样式则允许同时显示影像、道路和道路名称。

我们深入了解了 SwiftUI 中 MapKit 的强大功能,包括定制地图样式、交互方式和控件,为开发者提供了更多灵活性和可定制性的选择。

在 SwiftUI 中的作用域动画

作者 Swift社区
2023年12月7日 20:06

前言

从一开始,动画就是 SwiftUI 最强大的功能之一。你可以在 SwiftUI 中快速构建流畅的动画。唯一的缺点是每当我们需要运行多步动画或将动画范围限定到视图层次结构的特定部分时,我们如何控制动画。

简单示例

让我们从一个简单的示例开始,展示我们旧方法的一些缺点,这些方法用于在 SwiftUI 中驱动动画。

struct ContentView: View {
    @State private var isHidden = false
    
    var body: some View {
        VStack {
            Button("Animate") {
                isHidden.toggle()
            }
            
            HugeView()
                .opacity(isHidden ? 0.0 : 1.0)
                
            AnotherHugeView()
        }
        .animation(.default)
    }
}

如上例所示,我们有一个包含按钮和两个视图的视图层次结构,这些视图放置在垂直堆栈中。我们将动画视图修饰符附加到整个堆栈,以动画堆栈内的任何更改。

当我们按下按钮时,堆栈会动画显示内部的任何更改。但是,动画视图修饰符不连接到 isHidden 属性,这意味着它将动画显示可能发生的任何更改。其中一些更改可能是意外的,比如环境值的变化。

动画视图修饰符

我们可以通过使用动画视图修饰符的另一个版本来消除意外动画,在这个版本中,我们可以绑定到特定值,并且仅在值更改时进行动画处理。

struct ContentView: View {
    @State private var isHidden = false
    
    var body: some View {
        VStack {
            Button("Animate") {
                isHidden.toggle()
            }
            
            HugeView()
                .opacity(isHidden ? 0.0 : 1.0)
            
            AnotherHugeView()
        }
        .animation(.default, value: isHidden)
    }
}

在上面的示例中,我们使用了带有 value 参数的动画视图修饰符。它允许我们将动画范围限定为单个值,并仅在与特定值相关的更改时执行动画。在这种情况下,我们没有任何意外的动画。

使用多个可动画属性

如果我们有多个可动画属性怎么办?

在这种情况下,我们必须为每个可动画属性附加一个动画修饰符。这个解决方案非常有效,但在人体工程学方面有一个缺点。

struct ContentView: View {
    @State private var firstStep = false
    @State private var secondStep = false
    
    var body: some View {
        VStack {
            Button("Animate") {
                Task {
                    firstStep.toggle()
                    try? await Task.sleep(nanoseconds: 3_000_000_000)
                    secondStep.toggle()
                }
            }
            
            // 其他视图在这里
            
            SomeView()
                .opacity(firstStep ? 1.0 : 0.0)
                .blur(radius: secondStep ? 0 : 20.0)
        }
        .animation(.default, value: firstStep)
        .animation(.default, value: secondStep)
    }
}

幸运的是,SwiftUI 引入了动画视图修饰符的一个新变体,允许我们使用 ViewBuilder 闭包来限定动画的范围。

struct ContentView: View {
    @State private var firstStep = false
    @State private var secondStep = false
    
    var body: some View {
        VStack {
            Button("Animate") {
                Task {
                    firstStep.toggle()
                    try? await Task.sleep(nanoseconds: 1_000_000_000)
                    secondStep.toggle()
                }
            }
            
            // 其他视图在这里
            
            SomeView()
                .animation(.default) { content in
                    content
                        .opacity(firstStep ? 1.0 : 0.0)
                        .blur(radius: secondStep ? 0 : 20.0)
                }
        }
    }
}

如上例所示,我们使用动画视图修饰符,提供我们需要的动画类型和一个 ViewBuilder 闭包,在这个动画中应用。动画仅在提供的 ViewBuilder 闭包的上下文中工作,不会扩展到其他任何地方。

使用 ViewBuilder

作为起点,ViewBuilder 闭包提供一个参数,用于占位视图,在其中应用了动画视图修饰符。在 ViewBuilder 闭包内部,可以安全地对视图应用任何视图修饰符,并期望仅对此代码块进行动画处理。

struct ContentView: View {
    @State private var firstStep = false
    @State private var secondStep = false
    
    var body: some View {
        VStack {
            Button("Animate") {
                Task {
                    firstStep.toggle()
                    try? await Task.sleep(nanoseconds: 1_000_000_000)
                    secondStep.toggle()
                }
            }
            
            // 其他视图在这里
            
            SomeView()
                .transaction { t in
                    t.animation = t.animation?.speed(2)
                } body: { content in
                    content
                        .opacity(firstStep ? 1.0 : 0.0)
                        .blur(radius: secondStep ? 0 : 20.0)
                }
        }
    }
}

正如你所看到的,SwiftUI 提供了一种类似的方法,以在视图层次结构中维护有作用域的事务。

总结

这篇文章介绍了在SwiftUI中构建动画的新方法,重点解决了在多步动画或特定视图层次结构中控制动画的挑战。通过引入带有value参数的动画修饰符,以及使用ViewBuilder闭包限定动画范围,作者展示了更精确和灵活的动画控制方式。

这种方法在处理多个可动画属性时尤其强大。文章还提到了SwiftUI引入的一项新变体,使用ViewBuilder闭包可在动画中应用视图修饰符,有效地将动画范围限定在特定的上下文中。

最后,介绍了在 SwiftUI 中构建有作用域的事务的新方法,以维护更具精确性和可控性的动画。这些新功能在最新的平台上可用,为SwiftUI开发者提供了更强大的动画工具。

在 SwiftUI 中实战使用 MapKit API

作者 Swift社区
2023年11月30日 21:19

前言

SwiftUI 与 MapKit 的集成在今年发生了重大变化。在之前的 SwiftUI 版本中,我们将 MKMapView 的基本功能封装到名为 Map 的 SwiftUI 视图中。幸运的是,事情发生了变化,SwiftUI 引入了与 MapKit 集成的新 API。本篇文章我们将学习如何在 SwiftUI 的最新版本中使用可用的新功能丰富的 API 与 MapKit 集成。

正如我之前所说,在 SwiftUI 框架的早期版本中,我们有一个 Map 视图,为我们提供了 MapKit 的基本功能,该功能现在已被弃用。在面向较早 Apple 平台版本的情况下,仍然使用已弃用的 Map 视图是有意义的。

新 MapKit API 的引入

新的 MapKit API 引入了 MapContentBuilder 结果构建器,它看起来类似于 ViewBuilder,但是使用符合 MapContent 协议的类型。让我们从使用 SwiftUI 中最新迭代中提供的新 MapKit API 集成的基本示例开始。

import MapKit
import SwiftUI

extension CLLocationCoordinate2D {
    static let newYork: Self = .init(
        latitude: 40.730610,
        longitude: -73.935242
    )
    
    static let seattle: Self = .init(
        latitude: 47.608013,
        longitude: -122.335167
    )
    
    static let sanFrancisco: Self = .init(
        latitude: 37.733795,
        longitude: -122.446747
    )
}

struct ContentView: View {
    var body: some View {
        Map {
            Annotation("Seattle", coordinate: .seattle) {
                Image(systemName: "mappin")
                    .foregroundStyle(.black)
                    .padding()
                    .background(.red)
                    .clipShape(Circle())
            }
            
            Marker(coordinate: .newYork) {
                Label("New York", systemImage: "mappin")
            }
            
            Marker("San Francisco", monogram: Text("SF"), coordinate: .sanFrancisco)
        }
    }
}

正如你在上面的示例中看到的,我们通过使用 MapContentBuilder 闭包定义地图,并在其上放置内容。MapContentBuilder 类型与符合 MapContent 协议的任何类型一起使用。

在我们的示例中,我们使用了 Marker 和 Annotation 类型。Marker 是一个基本项,允许我们在地图上放置预定义的标记。Annotation 类型更先进,将使我们能够使用纬度和经度在地图上放置 SwiftUI 视图。

SwiftUI 为我们提供了许多符合 MapContent 协议的类型。我们已经使用了其中的两个:Marker 和 Annotation。其中许多包括 MapCircle、MapPolygon、MapPolyline、UserAnnotation 等。

struct ContentView: View {
    var body: some View {
        Map {
            Annotation("Seattle", coordinate: .seattle) {
                Image(systemName: "mappin")
                    .foregroundStyle(.black)
                    .padding()
                    .background(.red)
                    .clipShape(Circle())
            }
            
            Marker(coordinate: .newYork) {
                Label("New York", systemImage: "mappin")
            }
            
            UserAnnotation()
        }
    }
}

控制初始地图位置

你可以通过使用 Map 初始化器的另一个重载来控制地图的初始位置,该初始化器提供 initialPosition 参数。

struct ContentView: View {
    let initialPosition: MapCameraPosition = .userLocation(
        fallback: .camera(
            MapCamera(centerCoordinate: .newYork, distance: 0)
        )
    )
    
    var body: some View {
        Map(initialPosition: initialPosition) {
            Annotation("Seattle", coordinate: .seattle) {
                Image(systemName: "mappin")
                    .foregroundStyle(.black)
                    .padding()
                    .background(.red)
                    .clipShape(Circle())
            }
            
            Marker(coordinate: .newYork) {
                Label("New York", systemImage: "mappin")
            }
            
            Marker("San Francisco", monogram: Text("SF"), coordinate: .sanFrancisco)
        }
    }
}

initialPosition 参数接受 MapCameraPosition 类型的实例。MapCameraPosition 允许我们以几种方式定义地图位置。它可以是我们在示例中使用的用户位置,或者你可以使用 camera、region、rect 或 item 等静态函数将其指向地图上的任何区域。默认情况下,它使用 MapCameraPosition 类型的自动实例,该类型适合地图内容。

相机位置的双向绑定

每当你需要对相机位置有恒定的控制时,你可以使用 Map 初始化器的另一个重载,允许你提供与地图相机位置的双向绑定。

struct ContentView: View {
    @State private var position: MapCameraPosition = .userLocation(
        fallback: .camera(
            MapCamera(centerCoordinate: .newYork, distance: 0)
        )
    )
    
    var body: some View {
        Map(position: $position) {
            // ...
        }
    }
}

SwiftUI 在用户拖动地图时更新位置绑定。它还在你以编程方式更新 position 属性时立即更新地图相机位置。

struct ContentView: View {
    @State private var position: MapCameraPosition = .userLocation(
        fallback: .camera(
            MapCamera(centerCoordinate: .newYork, distance: 0)
        )
    )
    
    var body: some View {
        Map(position: $position, interactionModes: .pitch) {
            // ...
        }
    }
}

通过使用 interactionModes 参数,你可以控制与地图允许的交互类型。MapInteractionModes 类型定义了一组交互,如平移、俯仰、旋转和缩放。默认情况下,它启用所有可用的交互类型。

总结

今天,我们学习了在 SwiftUI 中集成 MapKit 的基础知识。在接下来的几周里,我们将继续讨论相机操作、地图控件和其他高级主题。希望你喜欢这篇文章。

Swift 周报 第四十二期

作者 Swift社区
2023年11月28日 17:08

前言

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

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

最热烈的火焰,封锁在最沉默的火山深处。最朴实纯真的智慧,就浅藏在Swift社区里!

周报精选

新闻和社区:苹果 CEO 库克透露接班计划,希望继任者来自公司内部

提案:Typed throws 提案正在审查

Swift 论坛:讨论 MainActor 上的上下文切换和线程数

推荐博文:SwiftUI 中的作用域动画

话题讨论:

那个活在记忆中的帅气少年,已慢慢变成了大叔模样。岁月无情呀,那么各位程序猿和攻城狮们,你们心中最担心的容貌变化是哪一个呢?

上期话题结果

这个结果反映了员工在工作和生活平衡方面的个体差异。一些人更注重通勤时间的利用效率,而另一些人则更注重在自己的房子中获得更大的舒适感和生活空间。这对公司提供灵活的工作安排和住房福利可能有一定的启示。

新闻和社区

苹果 CEO 库克透露接班计划,希望继任者来自公司内部

11 月 21 日消息,63 岁的苹果公司首席执行官蒂姆库克近日透露,苹果已经为他的继任者做好了 " 非常详细 " 的接班计划,但他也表示,他目前还没有离开苹果的打算。

在 BBC Sounds 播客《Dua Lipa: At Your Service》的一次 45 分钟的采访中,库克向歌手 Dua Lipa 坦承,他不知道自己还会在苹果待多久。" 我爱这里," 他说,回顾了自己在苹果的 25 年," 我无法想象没有苹果的生活,所以我还会在这里一段时间。" 但是,当被问及苹果是否有任何 CEO 接班计划时,库克称:" 我们是一家相信制定接班计划的公司,所以我们有非常详细的接班计划。因为总会发生一些不可预测的事情。我明天可能会走错路边,希望不会发生这种事,我祈祷不会。"

Dua Lipa 问道:" 你能说出谁是接班人吗?" 库克回答称," 我不能说,但我想说的是,我的工作是找到几个有能力接班的人,我真的希望下一任首席执行官是来自苹果内部的人。所以这是我的角色:让董事会有几个人可以选择。"

在这次采访中,库克讲述了自己作为苹果 CEO 的一天,分享了他在阿拉巴马州一个蓝领家庭长大的经历,以及最终成为苹果 CEO。(文章来源:IT 之家)

消息称苹果自研 5G 调制解调器开发再“难产”,将推迟到 2026 年

IT之家 11 月 17 日消息,彭博社的马克・古尔曼(Mark Gurman)发布最新一期 Power On 时事通讯,表示苹果的自研 5G 调制解调器计划遇到麻烦。

IT之家注:苹果公司于 2019 年收购了英特尔大部分智能手机业务,并开始认真开发自己的调制解调器硬件,但开发过程并不顺利。

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

苹果公司原本计划 2024 年推出自研 5G 调制解调器芯片,并率先装备在 iPhone SE 机型上,但随后有消息称延后到 2025 年。

古尔曼在最新时事通讯中表示,苹果计划再次延后推出自研 5G 调制解调器芯片时间,目前已经推迟到 2025 年年底或者 2026 年年初。

古尔曼在文章中透露,苹果的自研 5G 调制解调器芯片目前还处于早期阶段,可能落后竞争对手“数年”时间。

消息称苹果目前自研的 5G 调制解调器芯片并不支持 mmWave 技术,目前主要存在 2 个难题:第一是英特尔遗留代码,需要苹果重写,而添加新功能可能会中断现有功能;第二是开发芯片过程中,要小心绕过不侵犯高通的专利。

一位苹果员工表示:“我们接手了英特尔的一个失败项目,我们盲目自信地认为可以成功”。据说苹果的硬件技术部门在众多项目中“捉襟见肘”,各项资源没有向其倾斜,导致难以解决错误。

提案

正在审查的提案

SE-0413 Typed throws 提案正在审查。

Swift 的错误处理模型允许标记为 throws 的函数和闭包指示它们可以通过引发错误来退出。错误值本身始终被类型擦除为 any Error。这种方法鼓励以通用方式处理错误,并且对于大多数代码来说仍然是一个很好的默认选项。然而,有一些情况下类型擦除是不幸的,因为它不允许在可能且有必要处理所有错误的狭窄位置进行更精确的错误类型化,或者在类型擦除的成本很高的情况下。

该提案引入了指定函数和闭包只能引发特定具体类型错误的能力。

Swift论坛

  1. 提议多语句 if/switch/do 表达式

内容概括

该提案基于 SE-0380,引入了“then”关键字来处理 if 或 switch 表达式中的多个语句,从而促进更清晰的语法并提高可读性。 “then”关键字允许这些表达式每个分支有多个语句,从而简化了以前需要立即执行闭包或显式键入的场景。 此外,它还引入了“do”表达式,使代码结构更加清晰,并处理 API 需要价值创建和后续突变的情况。

该提案概述了详细设计,引入“then”作为上下文关键字,指定其在 if、switch 和 do 表达式中的用法。 它强调了解析歧义和可能的替代方案,探索诸如在 Swift 中使用最后一个表达式或受 Rust 启发的分号终止等变体,同时讨论它们对代码可读性和语言设计的影响。

总体而言,该提案旨在增强 Swift 的表达能力而不影响 ABI 稳定性,并邀请讨论引入的“then”关键字的替代方案和潜在的解析复杂性。

介绍

该提案引入了 then 关键字,用于确定单个分支中包含多个语句的 if 或 switch 表达式的值。 它还介绍了 do 表达式。

动机

SE-0380 引入了使用 if 和 switch 语句作为表达式的功能。 正如该提案所述,这可以大大改进语法,例如在初始化变量时:

let width = switch scalar.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: 4
}

否则需要诸如立即执行闭包或显式类型确定初始化之类的技术。 然而,该提案将让 switch 分支包含多个语句的能力作为未来的方向:

let width = switch scalar.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: 
      log("this is unexpected, investigate this")
      4  // error: Non-expression branch of 'switch' expression may only end with a 'throw'
}

当需要这样的分支时,当前用户必须退回到旧技术。 该提案引入了一个新的上下文关键字,它允许 switch 保留为表达式:

let width = switch scalar.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: 
      log("this is unexpected, investigate this")
      then 4
}

then 可以类似地用于允许 if 表达式中的多语句分支。 该关键字的引入还使得独立的 do 表达式更加可行。 它们有两个用例:

  1. 要从 do/catch 块的成功路径和失败路径生成值:
let foo: String = do {
    try bar()
} catch {
    "Error \(error)"
}
  1. 当使用单个表达式无法轻松完成变量初始化时,能够初始化变量:
let icon: IconImage = do {
    let image = NSImage(
                    systemSymbolName: "something", 
                    accessibilityDescription: nil)!
    let preferredColor = NSColor(named: "AccentColor")!
    then IconImage(
            image, 
            isSymbol: true, 
            isBackgroundSupressed: true, 
            preferredColor: preferredColor.cgColor)
}

虽然上面的内容可以组成一个表达式,但声明单独的变量然后使用它们会更清晰。

在其他情况下,这是无法完成的,因为 API 的结构要求您首先创建一个值,然后更改其中的一部分:

let motionManager: CMMotionManager = {
    let manager = CMMotionManager()
    manager.deviceMotionUpdateInterval = 0.05
    return manager
}()

这种立即执行的闭包模式在 Swift 代码中很常见。 以至于在某些情况下,用户认为即使是单个表达式也必须包含在闭包中。 do 表达式将提供更清晰的习惯用法来对这些进行分组。

  1. 讨论借用和输入输出模式匹配的设计问题

内容概括

讨论围绕着通过启用借用和输入输出模式匹配来增强 Swift 的模式匹配、允许在不复制或消耗值的情况下进行值匹配以及在模式匹配期间启用枚举的就地突变来增强 Swift 的模式匹配。 主要设计问题包括:

  1. 新的绑定模式:引入“借用 x”和“inout x”分别作为借用和变异模式绑定的语法。 这些将允许借用或改变部分匹配值而不消耗它。
  2. 模式的所有权行为:分析 Swift 中的各种模式类型以了解其所有权含义。 诸如绑定、通配符、元组、枚举、可选展开、布尔值、动态转换和表达式模式之类的模式将根据其所有权行为进行评估。
  3. 确定模式匹配所有权:探索确定模式匹配的所有权行为的方法。 聚合模式(元组和枚举)遵循其组件之间最严格的所有权行为:借用、变异或消费。
  4. 确定开关的效果:讨论如何通过句法标记或从应用模式推断所有权来确定开关对其主题的总体效果。 有人建议使用“&”标记来改变模式匹配。
  5. 条件中的所有权控制:考虑“if let”和“if case”构造中借用和 inout 模式绑定的含义。 这些新的绑定形式可用于可选展开,并且其行为类似于根据其所有权要求切换主题。

总体而言,我们的目标是在 Swift 中引入更细致的模式匹配,允许在不消耗值的情况下进行借用和变异,并探索这些增强功能在各种语言结构(如 switch 语句和条件)中的含义。

  1. 讨论如何依赖 SwiftPM 作为一个库?

问题

理论上,SPM 是一个普通的 swift 包,您可以将其(使用工具链附带的 SPM)构建为普通的 swift 包。但 swift-package-manager 存储库没有最新的 semver 标签,它使用“工具链”标记方案(swift-5.9.1-RELEASE)。 如何依赖 SPM 作为library?

回答

截至目前,libSwiftPM 尚未维护可以遵循语义版本控制的稳定 API。 您使用自己的 libSwiftPM 构建的软件包将从当前的 Swift 安装中提取 PackageDescription 模块,这可能与您使用的 libSwiftPM 版本不兼容。 这种不兼容性将表现为用于传递包清单和插件信息的不同序列化格式(本身是私有 API),这将导致模糊且难以诊断的错误。

作为以前维护过基于 libSwiftPM 构建的 CLI 工具,现在维护 SwiftPM 本身的人,我建议不要将其添加为依赖项。 它不适合在一起版本化并随 Swift 工具链分发的工具集之外使用。

如果您需要一个可以在包上操作的 CLI 界面,请改用 SwiftPM 命令插件,它们确实提供了稳定的 API。

  1. 讨论MainActor 上的上下文切换和线程数

提问

我正在观看 Swift 并发:幕后我了解到,作为使用 Swift 并发的开发人员,我们不应该违反不阻塞线程的运行时契约。 看来 Swift 的目标是运行与设备中 CPU 核心数量一样多的线程。 然而,会议结束时提出的一个观点引起了一些混乱。 演讲者提到,当我们调用 MainActor 的方法时,会发生上下文切换,因为主线程与协作池中的线程是分开的。 这引发了几个问题:

1、协作池中有多少个线程?

2、如果不包括主线程,这是否意味着实际的协作线程数是 numberOfCoresInDevice - 1?

3、为什么主线程不是协作池的一部分?

我的假设是,这可能是出于优化目的,允许主线程专注于 UI 任务; 否则,任何线程的任何继续都可以在挂起后在主线程上恢复。

4、这里是否违反了运行时契约:当我们将上下文切换到主线程时,我们当前的线程应该被阻塞?

5、或者这个合约只针对我们,开发者,系统可以随意违反吗?

无论如何,看起来在这种情况下我们有一个线程被阻塞。

也许,这个问题将作为前三个问题的答案得到回答,但无论如何:为什么主线程不能像协作池中的线程一样工作? 只是接收必须在主线程上执行的延续? 这将解决上下文切换问题。

回答

主线程主要通过 NSRunLoop 进行管理,因为它的存在时间比 Swift 存在的时间要长得多,更不用说 Swift 并发了。 当在默认模式下不可重入运行时,主调度队列由主运行循环提供服务。 在 Swift Concurrency 中,主要参与者的执行者负责将工作分派到该队列上,就像常规参与者的执行者(默认执行者)将工作分派到协作队列上一样,如您链接的文章中所述

但并非所有进程都有主线程; 它主要是一个与 UI 相关的概念,像守护进程这样的非 UI 进程不需要它。

  1. 讨论枚举案例关键路径:更新

内容概述

讨论围绕使用 Swift 宏增强对枚举的关键路径支持,特别是引入“案例关键路径”以更好地处理枚举案例。

  1. @CasePathable 宏:该宏为枚举案例生成实际的关键路径,称为“案例关键路径”。 这些关键路径提供动态案例查找功能,并且可以与常规关键路径类似地使用。
  2. 使用示例:@CasePathable 宏允许实现各种功能:
  • 通过下标访问枚举案例。
  • 使用 callAsFunction 嵌入新的有效负载。
  • 简化枚举案例检查和有效负载提取。
  • 利用 SwiftUI 绑定的大小写键路径,启用基于枚举大小写的导航和表单控件使用。
  • 使用大小写键路径组合应用程序功能,在构建和组合不同的应用程序功能时特别有用。
  1. 对库的影响:SwiftUINavigationComposable Architecture 等库已更新,以合并案例键路径,使用 Swift 键路径语法增强其功能、结构和可组合性。

提供的示例和案例研究旨在展示案例关键路径的多功能性和实用性,强调它们在简化代码、增强 SwiftUI 绑定、组合应用程序功能等方面的潜力。 希望展示这些用例将鼓励将案例关键路径纳入语言中,并激发进一步的创新应用程序。

案例研究:SwiftUI Bindings

大小写键路径使从枚举而不是一堆独立选项驱动 SwiftUI 导航成为可能。 例如,如果一个视图可以导航到两个不同的、互斥的功能,那么最好像这样建模:

struct FeatureView: View {
  @State var destination: Destination?

  enum Destination {
    case activity(ActivityModel)
    case settings(SettingsModel)
  }

  …
}

但构建对 Destination 枚举的每种情况的绑定可能很困难,以便您可以使用 sheet(item:)popover(item:)(以及更多)视图修饰符。 但是如果你的枚举用 @CasePathable 注释

@CasePathable
enum Destination {
// ...
}

然后我们可以利用绑定上的“动态大小写查找”,允许它们通过点链语法转换为 SwiftUI 现有视图修饰符所期望的形状:

.sheet(item: self.$destination.activity) { model in 
  ActivityView(model: model)
}
.popover(item: self.$destination.settings) { model in 
  SettingsView(model: model)
}

还可以使用 String 或 Bool 来驱动表单控件,例如 TextFields 和 Toggles,否则这些控件将被困在枚举案例中:

@CasePathable
enum Status {
  case inStock(quantity: Int)
  case outOfStock(isOnBackOrder: Bool)
}

@Binding var status: Status

switch self.item.status {
case .inStock:
  $status.inStock.map { $quantity in
    Section {
      Stepper("Quantity: \(quantity)", value: $quantity)
      Button("Mark as sold out") {
        status = .outOfStock(isOnBackOrder: false)
      }
    } header: { Text("In stock") }
  }
case .outOfStock:
  $status.outOfStock.map { $isOnBackOrder in
    Section {
      Toggle("Is on back order?", isOn: $isOnBackOrder)
      Button("Is back in stock!") {
        status = .inStock(quantity: 1)
      }
    } header: { Text("Out of stock") }
  }
}

如果您想尝试其中任何一个,我们的 SwiftUINavigation 库已更新,可以在使用 CaseKeyPath 进行绑定时定义动态成员查找。

案例研究:Composing App Features

近 4 年前我们开发案例路径的主要推动力是我们的可组合架构库,它提供了一种定义功能并将它们组合在一起的结构化方法。 功能使用枚举来枚举应用程序中所有可能的用户操作,并且这些枚举嵌套在父/子域层中,并且需要案例路径来编写可以将这些功能抽象地粘合在一起的代码。

我们还更新了该库以使用案例键路径,这允许人们通过使用简单且熟悉的键路径语法隔离子状态和操作来将功能组合在一起:

Reduce { state, action in 
   // ...
 }
-.ifLet(\.child, action: /Action.child) {
+.ifLet(\.child, action: \.child) {
   ChildFeature()
  }

这使我们能够利用本机键路径给我们带来的所有好处,例如 Xcode 自动完成和类型推断。

推荐博文

基于 UI 交互意图理解的异常检测方法

摘要: 本文介绍了利用页面多模态信息在UI测试领域的探索与实践经验。针对意图信息识别问题,我们利用图像+文本+渲染布局属性信息探索出了一种交互意图簇识别模型,验证了基于自注意力的多模态方向可行性。

此模型可以识别出渲染树元素多维度的意图属性信息,同时利用聚类算法将节点聚成交互意图簇,可以为后续的任务提供结构化决策信息。在标注数据较少的情况下仍体现了较好的准确率以及泛化能力。后续计划通过扩大数据集、加强预训练等方式继续提升模型识别的精度。

SwiftUI 中的作用域动画

摘要: 文章介绍了在 SwiftUI 中使用作用域动画的新方法。首先,我们回顾了以前在 SwiftUI 中处理动画的方式,并指出了其中的一些缺点。随后,我们展示了如何使用带有 value 参数的 animation 视图修饰符来限定动画范围,以及如何处理多个可动画属性的情况。

接着,我们介绍了 SwiftUI 中引入的 animation 视图修饰符的新变体,允许我们使用 ViewBuilder 闭包来限定动画范围。最后,我们还提到了在视图层次结构中维护作用域事务的方法。这些新方法为我们在 SwiftUI 中创建精确且有限范围的动画提供了更灵活的选择。

线程调度和 Actors 的执行方式

摘要: 本文讨论了在 Swift 中使用线程调度和 Actors 时的执行机制。Actors 可以确保代码在特定线程上执行,如主线程或后台线程,并帮助同步访问可变状态以防止数据竞争。

然而,开发人员常常误解 Actors 在非异步上下文中的线程调度,这是为了避免意外崩溃而至关重要的。作者建议在深入研究调度的具体细节之前,先阅读他的两篇文章:《Actors in Swift: how to use and prevent data races》和《MainActor usage in Swift explained to dispatch to the main thread》,因为它们会向您介绍 Actors 的概念。在本文中,探讨了调用带有任何 actor 属性标记的方法的影响。

在异步上下文中,文章讨论了使用 Actors 时的线程调度。通常情况下,您可能会在异步环境中使用 Actors 。如果您的调用代码访问带有 actor 属性的方法,您必须使用任务(task)或采用相同的全局 actor 。文章提供了相关的示例代码,并说明了编译器如何防止在非异步上下文中调度到 actor 线程。

话题讨论

那个活在记忆中的帅气少年,已慢慢变成了大叔模样。岁月无情呀,那么各位程序猿和攻城狮们,你们心中最担心的容貌变化是哪一个呢?

  1. 最担心越吃越肥胖,横向发展。
  2. 最担心逐渐变厚的高度镜片。
  3. 最担心青丝若雪,白发横生。
  4. 最担心秀发稀疏,日渐秃然。

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

关于我们

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

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

❌
❌