阅读视图

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

Swift学习总结——常见数据类型

1 常量

swift中,可以使用关键字let来声明常量。我们通过以下几个关键点来认识常量。

1.1 只能赋值一次

常量只能被赋值一次,如果重复赋值,会报错。以下图为例: image.png

1.2 常量在初始化之前,不能使用

如下面的示例,对常量a进行了声明,并且是Int型,但是没有初始化,直接使用则会报错。同样,变量age也没有初始化,直接使用也会报错。 image.png

声明即赋值,或者先声明使用前再赋值。如下面的两个示例:

        // 声明即赋值
        let age = 28
        print(age)
        
        // 先声明,使用前赋值
        let height : Int
        height = 175
        print(height)

1.3 不要求在编译时期确定,但使用之前必须赋值一次

示例中age3是一个常量,并通过一个函数赋值,所以在编译期间并没有确定值,而是在执行期间确定。 image.png

1.4 常量需要确定数据类型

常量在声明后,如果没有确定数据类型,也没有赋值,会报错。 image.png

2 标识符

  1. 比如常量、变量、函数,几乎可以使用任何字符

    下面的示例中,使用一些特殊的图案,来对常量、变量、函数进行命名: image.png

  2. 标识符不能以数字开头,不能包含制表符、空白字符、箭头等特殊字符

    标识符依然会有一些限制要求,比如不能以数字开头,不能使用制表符、空格等特殊字符。同时,在编码过程中,为了更加易于理解、扩展和维护,我们还是要遵循一些编码规范:

    • 清晰性:命名清晰,功能清晰
    • 可读性:便于快速理解方法作用
    • 可维护性:减少因命名歧义引发的 Bug,后期代码微调简单
    • 一致性:团队协作风格统一

3 常见数据类型

3.1 Objective-C数据类型

首先回顾一下Objective-C的数据类型。

  1. 基本数据类型 Objective-C的基本数据类型与C语言类似,主要包括:
    • 整型int(通常占用4字节,表示范围为-2,147,483,648至2,147,483,647)、short(通常占用2字节,表示范围为-32,768至32,767)、long(通常占用4或8字节,取决于平台)、long long(至少占用8字节,表示范围更大)。
    • 浮点型float(单精度浮点数,通常占用4字节)、double(双精度浮点数,通常占用8字节)、long double(高精度浮点数,具体大小依赖于编译器实现)。
    • 字符型char(通常占用1字节,用于存储单个字符)。
    • 布尔型BOOL(用于表示真假值,通常占用1字节)。
  2. 对象数据类型 Objective-C的核心特性之一是面向对象的数据类型,比如Foundation框架中提供的一些复合型对象:
    • 字符串‌:如NSString(不可变字符串)和NSMutableString(可变字符串)。
    • 数组‌:如NSArray(不可变数组)和NSMutableArray(可变数组)。
    • 字典‌:如NSDictionary(不可变字典)和NSMutableDictionary(可变字典)。
    • 数字‌:如NSNumber
    • 数据‌:如NSData
  3. 扩展数据类型 Objective-C还支持一些扩展的数据类型,包括:
    • 指针类型‌:如int*float*NSObject*等。
    • 结构体类型‌:如CGRectCGSizeCGPoint等。
    • 枚举类型‌:如NSComparisonResultUITableViewStyle等。
    • 类型定义‌:通过typedef关键字定义自定义数据类型,如typedef enumtypedef struct等。
    • 其他类型‌:如NSUIntegerNSIntegerSEL等。

3.2 Swift数据类型

Swift 数据类型主要包括基本数据类型(如整数、浮点数、布尔值、字符和字符串)和复合数据类型(如数组、元组、可选类型等)。

  1. 基本数据类型‌ 如整数、浮点数、布尔值、字符和字符串:

    • 整数类型‌: Int:平台相关长度(32位系统为32位,64位系统为64位),建议默认使用以提高代码一致性。‌‌变体:Int8 Int16 Int32 Int64(有符号)和 UInt8 UInt16等(无符号),需显式指定。‌‌
    • 浮点数类型Float:32位单精度浮点数(约6位小数精度)。‌‌Double:64位双精度浮点数(约15位小数精度,推荐默认使用)。‌‌
    • 布尔类型Bool:仅包含 truefalse,用于逻辑判断。‌‌
    • 字符与字符串Character:单个Unicode字符(如 "A")。String:文本数据,支持插值和多行语法(如 "Hello")。‌‌
  2. 复合数据类型‌ 数组、元组、可选类型等:

    • 数组与字典Array:有序同类型集合。‌‌ Dictionary:键值对集合(如 ["key": "value"])。‌‌
    • 元组与可选类型Tuple:异构值组合(如 (1, "error"))。‌‌ Optional:表示值可能存在为 nil(如 Int?)。‌‌
  3. 引用类型 类(class

3.3 Swift数据类型的底层实现

我们知道Objective-C对象数据类型class类底层实现是结构体‌,这个结构体的定义在runtime源码中。比如Foundation库中提供的类型,如NSStringNSArray等,以及我们自定义的继承自NSObject的类,这些底层都是结构体。在iOS底层学习——OC对象的本质与isa中也有过探索。

Swift中,引用类型类(class)底层并不是结构体来实现的,但是一些基本数据类型如IntFloatDoubleCharacterString以及ArrayDictionary却是定义为结构体主要是因为它们作为值类型在性能和内存管理上具有优势。

下图对swift数据类型进行了归类: image.png

我们在开发工具上,也能够看到,Int是一个结构体: image.png

快速跳转到定义(Jump to Definition),Command + 点击鼠标左键快捷键,也能看到,源码中Int被定义成为一个public型的结构体。 image.png

我们知道结构体占用的内存空间,取决于结构体内部所有变量占用空间之和,这里提出一个疑问:将这些基本数据类型定义成结构体,难道不会增加复杂度,增加内存使用吗?其实swift内部做了优化,后面我们再深入探索。

4 部分数据类型的使用

4.1 整型类型

  • Swift提供了多种整型类型,如Int8Int16Int32Int64UInt8UInt16UInt32UInt64
  • Int8表示占8位,1个字节;UInt8U表示无符号。
  • 32bit平台,Int等价于Int3264bit平台,Int等价于Int64
  • 一般情况下,我们使用Int即可,除非对内存占用空间有强制性要求。

可以通过maxmin属性,了解数据类型对应的最大值和最小值。(注意这里的maxmin是属性,不是函数方法) image.png

4.2 浮点型

  • Float 32位,精度只有6位
  • Double 64位,精度至少15位
  • 初始化一个浮点型时,如果没有声明类型,默认是Double

image.png

4.3 不同进制表示方式

  • let intDecimal = 17 // 十进制
  • let intBinary = 0b10001 // 二进制
  • let intOctal = 0o21 // 八进制
  • let intHexadecimal = 0x11 // 十六进制

4.4 字符型

  • 字符型和字符串一样,使用双引号“”
  • 初始化一个字符型时,如果没有声明类型,默认是String
  • 字符可以存储ASCII字符Unicode字符

image.png

4.5 数组、字典

  • let array = ["a", "b", "c"]
  • let dic = ["a" : 12, "b" : 13, "c" : 21]
  • 字典类型也是使用[]

在原生的容器类型中,他们都是泛型的,也就是我们在一个集合中,只能放同一种类型的元素。 image.png

如果我们要把不相关的类型,放到同一个容器类型中的话,比较容易想到的是使用 AnyAnyObject,如下面的示例,再或者使用NSArrayNSDictionary

  • let array : [Any] = ["a", "b", "c", 1]
  • let dic : [String : Any] = ["a" : 12, "b" : "13"]

image.png

4.6 类型转换

  1. 整数转换

    如下图的示例中,age1age2虽然都是整型,但是其占用的存储空间是不同的,所以是不能直接相加的。 image.png

    因为Int8占用8位一个字节,而Int16占用两个字节,所以可以将age1转换为Int6,再相加,而age3也自动变成Int16型。 image.png

  2. 整数、浮点数转换

    如下图所示,整型和浮点型类型不批配,是不可以直接相加的: image.png

    可以将Int型转为浮点型,此时intp也为Double型: image.png

    但是下面这种方式是可以的,字面量可以直接相加,因为数字字面量本身没有明确的类型: image.png

4.7 元祖tuple

将多个不同数据类型组合赋予一个变量,可以通过序号来访问对应位置上的值image.png

可以将定义的元祖赋予一个元祖,对应位置上元素会自动赋值: image.png

如果元素不需要赋值,可以直接用_来代替: image.png

在定义元祖时,还可以给每个元素设置一个key: image.png

Swift 6 新特性(一):count(where:) 方法带来的从复杂到简洁变化

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

前言

Swift 语言一直在不断演进,推出的新特性不仅提升了性能,还提高了代码的可读性。

其中一个值得关注的新功能就是在 SE-0220 中引入的 count(where:) 方法。这个方法让我们可以更高效、更具表现力地统计序列中满足特定条件的元素,避免了之前需要结合 filter()count 的复杂操作。

count(where:) 方法将过滤(filter)和计数(count)两个步骤合并为一个,省去了创建和丢弃中间数组的麻烦,从而提升了性能,同时代码也变得更加简洁。

示例1:统计高于冰点的温度

假设我们有一个以摄氏度为单位的温度数组,我们想统计其中有多少温度高于冰点(0°C):

在之前,我们可能需要这样写:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.filter { $0 > 0 }.count

现在,我们可以使用 count(where:) 方法来简化代码:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.count { $0 > 0 }

// 输出 `3`
print(aboveFreezingCount)

在这个例子中,aboveFreezingCount 的值为 3,因为有三个温度(10, 20, 25)符合条件。

示例2:统计具有特定前缀的元素

让我们再看一个示例,假如我们有一组产品名称,想统计以 "Apple" 开头的产品数量:

let products = [
    "Apple", 
    "Banana", 
    "Apple Pie",
    "Cherry", 
    "Apple Juice", 
    "Blueberry"
]
let appleCount = products.count { $0.hasPrefix("Apple") }

// 输出 `3`
print(appleCount)

在这个例子中,appleCount 的值为 3,因为 "Apple"、"Apple Pie" 和 "Apple Juice" 都以 "Apple" 开头。

示例3:根据长度统计元素

一个常见的应用场景是根据元素的长度进行统计。例如,我们可能想知道数组中有多少名字的长度少于六个字符:

let names = ["Natalia""Liam""Emma""Olivia""Noah""Ava"]
let shortNameCount = names.count { $0.count < 6 }

// 输出 `4`
print(shortNameCount)

在这个例子中,shortNameCount 的值为 4,因为 "Liam"、"Emma"、"Noah" 和 "Ava" 的长度都少于六个字符。

示例4:统计特定元素

如果需要统计序列中某个特定元素出现的次数,可以在闭包中使用等于运算符 (==)。例如:

let animals = ["cat""dog""cat""bird""cat""dog"]
let catCount = animals.count { $0 == "cat" }

// 输出 `3`
print(catCount)

在这个例子中,catCount 的值为 3,因为 "cat" 在数组中出现了三次。

适用范围和平台支持

count(where:) 方法适用于所有遵循 Sequence 协议的类型,这意味着我们不仅可以在数组中使用,还可以在集合、字典和其他序列类型中使用。

但需要注意的是,序列必须是有限的,以确保方法能够在合理的时间内完成。

count(where:) 方法在 Swift 6 中引入,因此需要 Xcode 16 才能使用这个特性。它支持多种平台和操作系统版本,包括 iOS 8.0+、macOS 10.10+、visionOS 1.0+ 等。

总结

count(where:) 方法是一个非常实用的功能,它不仅简化了代码,还提高了性能。如果你是一名经验丰富的 Swift 开发者,想要学习高级技巧,可以关注我的公众号,我会持续分享 Swift 相关的技巧和知识。

你对这个新特性有什么看法呢?欢迎在评论区与我们分享你的想法。

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

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

为什么那些看起很丑的产品却能上架AppStore?

背景

关于AppStore允许怎样的产品过审,其实一直具有玄学性。这里主要是受审核员的心情和工作态度影响最大

最常见的莫过于上一版本过审,简单的更新关键词或者维护一个小Bug,遭受被拒审核的痛苦折磨,乃至3.2f致命打击。常见的场景如下:

  • 审核员用最新系统测试导致闪退被拒
  • 测试账户登录异常
  • 社区发帖功能要求登录
  • 社区帖子缺啥举报、拉黑功能
  • 页面空白

另外,统一回复一下最近提问最多的问题,那就是为什么那些看起来很丑的产品却能上架?直接上图。

示例产品一

示例产品二

示例产品三

为什么?

⚠️事先声明本文绝无攻击以上产品的言论和恶意,仅用来参考并客观讲述AppStore审核策略

从审美的角度来看,上面列举的产品其实并不够精品。但是对于AppStore来说是绝对合规的。

那么可能有同行会诡辩地说了,这是AppStore故意丑化大陆地区的形象。

其实不然,无论是代码层面还是设计层面,你不得不承认这样的设计其实也是一种标新立异对于破局4.3(a)肯定是有作用的

首先这种色块版的设计,并不是适用于所有产品。从哲学存在即合理的角度来说,这部分工具类的产品本身并不需要花里胡哨的设计,增加用户的教育成本。仅需这种朴华务实的卖点足以解决用户的痛点

其次,这种设计理念本身也是敢为天下先,与其美的千篇一律,不如“丑”的千奇百怪

风险点

这种产品的风险点在于后续的迭代与转型,只能说这样的设计可以破局0~1的尴尬处境,但是在后续的迭代依旧会存在4.3(a)的风险。陷入进退两难导致不改不够精美,改了不好过审的尴尬处境

所以,这种取巧的技巧更适合于工具类产品,如果是小而美的产品并完全适用。毕竟在弯道超车翻车的不在少数。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

Xcode 14.3 和 iOS 16.4 为 SwiftUI 带来了哪些新功能?

在这里插入图片描述

0. 概览

今年年初,Apple 推出了最新的 Xcode 14.3 以及对应的 iOS 16.4 。

与此同时,它们对目前最新的 SwiftUI 4.0 也添加了一些新功能:

  • sheet 弹窗后部视图(Interact with a view Behind a sheet)可交互;
  • sheet 弹窗背景透明化;
  • 调整 sheet 弹窗顶角弧度;
  • 控制弹窗内滚动手势优先级;
  • 定制紧密(compact-size )尺寸下 sheet 弹窗大小;
  • Xcode 预览(Preview)模式下对调试输出的支持;

让我们依次来了解一下它们吧。

Let‘s go!!!;)


1. sheet 后部视图可交互

在 iOS 16.4 之前,SwiftUI 中 sheet 弹窗后,如果点击其后部的视图会导致弹窗立即被关闭,从而无法与弹窗后部的视图进行交互。

从 iOS 16.4 开始,我们可以为 sheet 弹窗应用 presentationBackgroundInteraction() 方法,以达到不关闭弹窗而与后部视图交互之目的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isPresented = false
    @State private var number = 0
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            Button("Sheet") {
                isPresented = true
            }
            .buttonStyle(.borderedProminent)
            .padding()
            
            VStack {
                Button("产生随机数: \(number)"){
                    number = Int.random(in: 0..<10000000)
                }
                .foregroundColor(.white)
                .font(.title.weight(.black))
            }.padding(.top, 200)
        }
        .sheet(isPresented: $isPresented) {
            Text("大熊猫侯佩 @ csdn")
                .font(.headline)
                .presentationDetents([.height(120), .medium, .large])
                // 开启后部视图交互
                .presentationBackgroundInteraction(.enabled)
        }
    }
}

在这里插入图片描述

2. sheet 背景透明化

从 iOS 16.4 开始,我们可以为 sheet 弹窗选择透明样式,更好的美化弹出窗口的显示效果。

如下代码所示,我们在 sheet 弹窗上应用了 presentationBackground(_: ) 修改器以实现透明磨砂效果:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetTransparency = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        // 或使用 .background 调用 presentationBackground() 方法效果相同
                        //.presentationBackground(.background)
                }
                
                Spacer()
                
                Button("透明弹出") {
                    isSheetTransparency = true
                }
                .sheet(isPresented: $isSheetTransparency) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationBackground(.ultraThinMaterial)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

3. sheet 顶部弧度调整

感觉 sheet 弹窗顶角生硬无弧度的小伙伴们有福了,从 iOS 16.4 开始,SwiftUI 开始支持调整 sheet 弹出窗口顶角的弧度了。

我们可以使用 .presentationCornerRadius() 修改器来实现这一功能:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetRadius = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents(.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("顶角圆润弧度弹出") {
                    isSheetRadius = true
                }
                .sheet(isPresented: $isSheetRadius) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationCornerRadius(30.0)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

4. sheet 滚动手势优先级调整

在 iOS 16.4 之前,如果我们 sheet 尺寸可变弹窗中包含滚动视图(比如 List,ScrollView 等),当用户在弹窗中滚动将会首先引起弹窗尺寸的改变,而不是其滚动内容的改变。

在 iOS 16.4 之后,我们可以调整 sheet 弹窗滚动手势优先级,以确保首先滚动其内容而不是改变弹窗尺寸。

这是通过 .presentationContentInteraction(.scrolls) 方法来实现的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetScrollable = false
    
    var body: some View {
        
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("滚动高优先级弹出") {
                    isSheetScrollable = true
                }
                .sheet(isPresented: $isSheetScrollable) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                    .presentationContentInteraction(.scrolls)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

5. 定制 sheet 在紧密尺寸下的大小

在 iOS 16.4 之前,如果在 iPhone 横屏时 sheet 弹窗,则弹出窗口将会铺满整个屏幕。

从 iOS 16.4 开始,我们可以为弹窗应用新的 .presentationCompactAdaptation(_: ) 修改器来改变横屏时弹窗的大小:

struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(spacing: 16) {
            Text("大熊猫侯佩 @ csdn")
            Button("关闭"){
                dismiss()
            }
        }
    }
}

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetCompactSizeCustom = false
    
    var body: some View {
                
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    SheetView()
                        .padding()
                        .frame(width: 200)
                        .presentationDetents([.height(200), .medium, .large])
                }
                
                Spacer()
                
                Button("自定义尺寸弹出") {
                    isSheetCompactSizeCustom = true
                }
                .sheet(isPresented: $isSheetCompactSizeCustom) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                    }
                    .padding()
                    .frame(width: 350)
                    .presentationDetents([.height(200), .medium, .large])
                    .presentationCompactAdaptation(.sheet)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

6. Xcode 预览模式对调试输出的支持

Xcode 14.3 之前,我们在预览(Preview)模式中测试 SwiftUI 界面功能时无法观察调试语句( print 等方法)的输出结果,必须在模拟器或真机中运行才可以在 Xcode 底部调试小窗口中看到 print() 等方法的输出。

从 Xcode 14.3 开始,以预览模式运行 App 时也可以在调试窗口中看到调试语句的输出了,真是太方便了:

@available(iOS 16.4, *)
struct ContentView: View {
    
    var body: some View {
                
        ZStack(alignment: .center) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            Button("显示 debug 输出") {
                print("显示随机数: \(Int.random(in: 0..<10000000))")
            }
        }
    }
}

在这里插入图片描述

7. 总结

在本篇博文中,我们介绍了在 Xcode 14.3 和 iOS 16.4 中 SwiftUI 为开发者带来的新方法和新功能,解决了诸多燃眉之急的问题,小伙伴们不想赶快尝试一下吗?🚀

感谢观赏,再会!8-)

有用的知识又增加了:为何无法编译某些  WWDC 官方视频中的代码?

在这里插入图片描述

概览

作为 Apple 开发者而言,每期 WWDC 官方视频无疑是我们日常必看的内容。

不过,小伙伴们是否发现视频中有些示例代码在我们自己测试时却无法编译这一尴尬的情况呢?

在本篇博文中,我们将通过一则非常简单的示例来向大家展示为什么会出现这种情况,以及如何解决它!

闲言不再,Let‘s go!!!;)


无法编译! 搞什么飞机?

Apple 在 WWDC21 关于 SwiftUI 3.0 的介绍视频中,曾经谈到了 Button 视图新的显示方式。

其中, 该演示视频强调过,码农们可以用新加入的 .buttonStyle(.bordered)、 .controlSize(.large) 以及 .controlProminence(.increased) 修改器方法来进一步增强按钮的外观显示:

Button {
        ...
  } label: {
    Text("Continue")
      .frame(maxWidth: .infinity)
  }
  .buttonStyle(.bordered)
  .controlSize(.large)
  .controlProminence(.increased)
  .padding(.horizontal)

按钮的显示应该为如下效果:

在这里插入图片描述

不过,现在我们在 Xcode 14.3 中编译如上代码,妥妥的会报错:

在这里插入图片描述

因为 SwiftUI 根本找不到 controlProminence() 这个方法!这是怎么回事呢?

Apple 的“小心机”

Apple 在每次 WWDC 视频中都会展示一些新的功能,其中包括一些新的方法,属性和类等。

不过,上述这些内容并不都是“板上钉钉”的事,有些可能会在正式代码中做出修改甚至删减。

Apple 这种又想“炫酷”又时常“翻脸不认人”的行为,对我们这些秃头码农来说是非常蛋疼的。

在这里插入图片描述

博文开头的“惨案”就由此而引出。

在 WWDC 视频发布时苹果“一拍脑袋”想出了个 controlProminence() 方法来渲染背景突出按钮的显示效果,可在 SwiftUI 3.0 正式发布时却觉得不妥对其做了人道毁灭!

都快两年了,你好歹也更新一下原来错误的视频啊!不可能!这对 Apple 来说绝不可能!!!

在这里插入图片描述

解决之道

所幸的是,诸如此类问题我们都可以自行搜索到解决之法,比如在一些技术大牛(比如我 ;-) )的博客、stackoverflow、Apple 官方论坛、某哥里等等。

拿上面的问题来说吧,Apple 连吱都不吱一声就删除了 controlProminence() 方法,而将 SwiftUI 按钮背景突出显示的功能放在了 buttonStyle 的 borderedProminent 样式中,你说气人不气人 :-|!

所以,原来的代码现在应该修改为如下样式:

Button {} label: {
    Text("Continue")
      .frame(maxWidth: .infinity)
  }
  .buttonStyle(.borderedProminent)
  .controlSize(.large)
  .padding(.horizontal)

现在疑惑终于解开了,我们又可以边掉头发边观赏可能有些许“变质”的 WWDC 官方视频了!棒棒哒!💯

总结

在本篇博文中,我们讨论了为何有些  官方 WWDC 视频中的示例代码无法编译通过的问题,并给出解决思路。

感谢观赏,再会 8-)

iOS 17(SwiftUI 5.0)带来的图表(Charts)新类型:“大饼”与“甜甜圈”

在这里插入图片描述

概览

WWDC 2023 为我们带来了 iOS 17,也为我们带来了 SwiftUI 5.0。

在 SwiftUI 新版中,Apple 增加了很多重磅功能,也对原有功能做了大幅度升级。

对于 Charts 框架, 新增了饼图(Pie)类型并且加入了图表元素的原生选择功能。

在这里插入图片描述

在本篇博文中,就让我们一起来看看 SwiftUI 5.0 中这些激动人心的新功能吧!

  1. "大饼"与"甜甜圈"
  2. 图表元素的选中
  3. 填上 WWDC 23 视频中的“坑”

Let's go!!!:)


1. "大饼"与"甜甜圈"

SwiftUI 5.0 在 4.0 众多图表类型基础之上,增加了全新的 饼图(Pie) 类型,我们可以通过它来更形象的展示图表数据。


注意:本文中的代码需要 Xcode 15 beta 版才能编译和运行。


下面是 SwiftUI 4.0 Charts 条状图的展示:

在这里插入图片描述

代码如下:

@Model
final class Item {
    var name: String
    var power: Int
    var timestamp: Date
    
    init(name: String, power: Int) {
        self.name = name
        self.power = power
        timestamp = Date.now
    }
}

Chart(items) { item in
    BarMark(x: .value("power", item.power), stacking: .normalized)
        .foregroundStyle(by: .value("name", item.name))
}
.chartLegend(.hidden)

想改为使用新饼图类型非常简单,只需将上面的 BarMark 换为 SectorMark 即可:

SectorMark(angle: .value("power", item.power))

在这里插入图片描述

我们可以调整每块“大饼”的空隙大小(angularInset)和圆角的弧度(cornerRadius):

SectorMark(angle: .value("power", item.power),angularInset: 3.0)
    .cornerRadius(10)

在这里插入图片描述

值得注意的是:Charts 中饼图数据改变的动画效果做的也非常生动,SwiftUI 会自动根据状态的变化来合成自然的动画,无需多写半行代码。

不过,“大饼”虽好,“甜甜圈”更佳!

小孩子才做选择,光有“大饼”怎么行,我们连“甜甜圈”也统统都要了🍩!

实现“甜甜圈”(饼图空心)效果也很容易,我们只需调整 SectorMark 构造器中 innerRadius 属性的值即可:

SectorMark(angle: .value("power", item.power),
           innerRadius: .ratio(innerRadius),
           angularInset: 3.0
)

在这里插入图片描述

好诱人的“甜甜圈”哦,有没有想吃的欲望呢?;)

2. 图表元素的选中

除了加入新图表类型以外,SwiftUI 5.0 中 Charts 终于可以支持原生选择啦!

现在,我们无需再手动计算是图表中哪个元素被选中了,一切回归简洁:

struct LocationDetailsChart: View {
  @Binding var rawSelectedDate: Date?

  var body: some View {
    Chart {
      ForEach(data) { series in
        ForEach(series.sales, id: \.day) { element in
          LineMark(
            x: .value("Day", element.day, unit: .day),
            y: .value("Sales", element.sales)
          )
        }
        .foregroundStyle(by: .value("City", series.city))
        .symbol(by: .value("City", series.city))
        .interpolationMethod(.catmullRom)
      }
    }
    .chartXSelection(value: $rawSelectedDate)
  }
}

如上代码所示,我们使用 chartXSelection(value:) 修改器方法将当前选中的数据放入指定的绑定($rawSelectedDate)中。

在这里插入图片描述

除了选择单个图表元素,我们还可以选择一段范围内的元素集合:

Chart(data) { series in
  ForEach(series.sales, id: \.day) { element in
    LineMark(
      x: .value("Day", element.day, unit: .day),
      y: .value("Sales", element.sales)
    )
  }
  ...
}
.chartXSelection(value: $rawSelectedDate)
.chartXSelection(range: $rawSelectedRange)

在这里插入图片描述

那么问题来了,能不能选中 SwiftUI 5.0 图表新饼图类型的“大饼”元素呢?答案是肯定的!

下面是官方视频中对应的代码:

Chart(data, id: \.name) { element in
  SectorMark(
    angle: .value("Sales", element.sales),
    innerRadius: .ratio(0.618),
    angularInset: 1.5
  )
  .cornerRadius(5)
  .foregroundStyle(by: .value("Name", element.name))
  .opacity(element.name == selectedName ? 1.0 : 0.3)
}
.chartAngleSelection(value: $selectedAngle)

类似的, 通过 chartAngleSelection(value:) 修改器方法实现了饼图元素的选中:

在这里插入图片描述

不过,单从这段代码我们还是无法了解饼图元素选中的实现细节,比如:selectedAngle 是什么?它是如何转换成 selectedName 的呢?

为什么  在此要“犹抱琵琶半遮面”隐藏相关的细节呢?这不禁让我预感到它会是一个“坑”!

“坑”中的实现很可能在 iOS 17 正式版中会有所不同,所以  才会这样“遮遮掩掩”。


想要了解更多相关的内容,请移步如下链接观赏:


3. 填上 WWDC 23 视频中的“坑”

WWDC 23 中对应内容的官方视频在下面,想要了解来龙去脉的小伙伴们可以“肆意”观赏:

尽管官方视频中的代码对如何完成饼图元素选中功能“闪烁其词”,但我们可以自己发挥“主观能动性”来大胆推测一下它的实现细节:即自己搞定“甜甜圈”的选中功能。

首先我们要搞清楚的是, chartAngleSelection 方法参数中的绑定值到底是个啥:

public func chartAngleSelection<P>(_ binding: Binding<P?>) -> some View where P : Plottable

我们可以通过监视 angleValue 的值,来看看它是如何跟随我们点击而变化的:

struct ContentView: View {
    // 省略其它状态定义...
    @Query private var items: [Item]
    @State private var angleValue: Int?
    
    var body: some View {
        NavigationView {
            List {
                Chart(items) { item in
                    SectorMark(angle: .value("power", item.power),
                               innerRadius: .ratio(innerRadius),
                               angularInset: 3.0
                    )
                    .cornerRadius(10)
                    .foregroundStyle(by: .value("name", item.name))
                }
                .chartLegend(.hidden)
                .chartAngleSelection($angleValue)
                .onChange(of: angleValue){ old,new in
                // 探查 angleValue 的真正面目...
                    print("new angle value: \(new)")
                }.padding(.vertical, 50)
                
                ForEach(items) { ... }
            }
            .navigationTitle("饼图演示")
        }
    }
}

在这里插入图片描述

如上图所示:chartAngleSelection($angleValue) 方法中的绑定是一个数量值(定义成浮点数类型也可以),我们还发现 angleValue 在 0° 位置附近点击时值越小,而在 360° 位置点击时值越大。

经过验证可得:angleValue 最大值就是 items 中所有元素 power 值的和!据此,我们可以轻松写一个从 angleValue 值找到对应选中 item 的方法:

private func findSltItem() -> Item? {
    guard let slt = angleValue else { return nil }
    
    var sum = 0
    // 若 angleValue 小于第一个 item.power ,则表示选择的是图表中首张“大饼”!
    var sltItem = items.first
    for item in items {
        sum += item.power
        // 试探正确选中的饼图元素
        if sum >= slt {
            sltItem = item
            break
        }
    }
    return sltItem
}

我们现在可以根据饼图中当前选中的 angleValue 值,轻松找到对应的 Item 了:

struct ContentView: View {
    // 省略其它状态定义...
    @Query private var items: [Item]
    @State private var angleValue: Int?
    @State private var sltItem: Item?
    
    var body: some View {
        NavigationView {
            List {
                Chart(items) { item in
                    SectorMark(angle: .value("power", item.power),
                               innerRadius: .ratio(innerRadius),
                               angularInset: 3.0
                    )
                    .cornerRadius(10)
                    .foregroundStyle(by: .value("name", item.name))
                    .opacity(sltItem?.id == item.id ? 1.0 : 0.3)
                }
                .onChange(of: angleValue){ old,new in
                    withAnimation {
                        if let item = findSltItem() {
                            if item == sltItem {
                                // 点击已被选中的元素时取消选择
                                sltItem = nil
                            }else{
                                sltItem = item
                            }
                        }
                    }
                }.padding(.vertical, 50)
                
                ForEach(items) {...}
            }
            .navigationTitle("饼图演示")
        }
    }
}

效果如下:

在这里插入图片描述

看来为  WWDC 官方代码填坑的感觉也很不错哦😘💯

总结

在本篇博文中,我们介绍了 WWDC 23 最新 SwiftUI 5.0(iOS 17)中关于图表的新体验,学习了如何创建饼图(Pie)和实现 Charts 元素的选中功能,小伙伴们还不赶快操练起来!

感谢观赏,再会!8-)

Xcode 高效秘诀:这 11 个快捷键你必须知道!

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

前言

作为一个 iOS 开发者,在使用 Xcode 时,掌握键盘快捷键是提高生产力和效率的关键。在这篇文章中,我将为大家介绍一些我最喜欢的 Xcode 快捷键。

其实之前也写过一些相关的文章,感兴趣的也可以去看看:

快捷键速查表

在开始之前,先来一个快捷键速查表:

  • ⌘ - Command

  • ⇧ - Shift

  • ⌥ - Option/Alt

  • ⌃ - Control

现在,让我们一起深入了解吧。

1. 产品菜单快捷键

首先,让我们从 Xcode 的产品菜单中一些基础快捷键开始:

  • 运行:⌘ R

  • 测试:⌘ U

  • 清理构建文件夹:⇧ ⌘ K

  • 清理 DerivedData 文件夹:⌘ ⇧ K

  • 停止:⌘ .

如果你已经是一个 iOS 开发者了,相信这些快捷键你已经很熟悉了,下面我们再介绍一些更高级的快捷键。

2. 快速导航

在处理不同文件,或者跟踪调用栈来解决问题时,快速导航是节省时间的法宝:

  • 前进:⌃ ⌘ →

  • 后退:⌃ ⌘ ←

3. 快速打开与跳转定义

另一个重要的快捷键是快速打开文件,使用 ⇧ ⌘ O,这个快捷键不仅可以搜索文件,还可以搜索类名和方法。

image.png

使用此快捷键后,跳转到定义,然后如果你想知道当前方法的所在的文件,可以使用 ⌃ ⌘ J 快捷键。

4. 查找功能

无论是在当前打开的文件中,还是在整个项目/工作区内,查找都是必不可少的操作:

  • 当前文件查找:⌘ F

  • 当前文件查找并替换:⌥ ⌘ F

  • 全局查找:⇧ ⌘ F

5. 视图管理

能够快速显示和隐藏不同的 Xcode 区域特别有用,尤其是在较小屏幕上工作时:

  • 显示/隐藏项目导航器:⌘ 0

  • 显示/隐藏调试控制台:⌘ ⇧ Y

  • 显示/隐藏检查器:⌘ ⌥ 0

6. 快速打开两个文件并排显示

我之前提到过的快速打开命令 ⌘ ⇧ O,在结合使用 ⌥ 时更加强大。

  • 使用 ⌘ ⇧ O 打开对话框

  • 输入你要查找的文件名

  • 按住 ⌥ 键然后用鼠标单击目标项目,或者使用 enter 键选择文件。新文件将会在一个单独的编辑器中打开,这样你可以继续在当前文件上工作,同时访问新打开的文件。

这个 ⌥ 技巧在项目导航器中选择文件时也同样适用。

7. 快速跳转到文件中的特定方法

对于大型文件中有众多方法的情况,这个快捷键非常有用,因为滚动很快会变得繁琐。

  • 使用 ⌃ 6 打开文档结构

  • 开始输入方法名

  • 使用 enter 键跳转到该方法

image.png

在输入时还可以模糊匹配,Xcode 会为你完成搜索。

8. 重复上一次测试

如果你经常写单元测试,快捷键 ⌃ ⌘ ⌥ G 在特别有用,可以重复运行我们上次进行的测试。

9. 重新缩进代码

我们可以通过按 ⌃ I 来重新缩进选定的代码,这在代码位置混乱时(例如在重构之后)特别有用。

我一般会配合全选快捷键 ⌘ A 一起使用,先全选再重新缩进,这样就可以将整个文件的代码进行重新缩进了。

10. 启用/禁用断点

在调试代码时,快捷键 ⌘ Y 可以帮助我们快速启用和禁用断点。

11. 不编译运行

这个快捷键非常有用,但可能很多人不知道。

在开发过程中,我们通常使用 ⌘ R 来运行代码,但这个命令其实是先编译再运行,但有时候我们并不需要编译,比如我刚执行完 ⌘ B,或者刚刚运行完没改代码的情况下想再运行一次。

这时候就可以使用 ⌃ ⌘ R 快捷键,直接运行,不用重新编译,非常节省时间。

总结

通过掌握这些快捷键,你可以大大提高在 Xcode 中的工作效率,节省宝贵的时间,让开发过程更加顺畅。希望这些快捷键能为你的开发旅程带来更多便利,你还有哪些喜欢的快捷键,欢迎在评论区留言分享。

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

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

Swift 新并发模型中 isolated 和 nonisolated 关键字的含义看这篇就懂了!

在这里插入图片描述

概览

在 Swift 新 async/await 并发模型中,我们可以利用 Actor 来避免并发同步时的数据竞争,并从语义上简化代码。

Actor 伴随着两个独特关键字:isolatednonisolated,弄懂它们的含义、合理合规的使用它们是完美实现同步的必要条件。

那么小伙伴们真的搞清楚它们了吗?

在本篇博文中,您将学到如下内容:

  1. isolated 关键字
  2. nonisolated 关键字
  3. 没有被 async 修饰的方法也可以被异步等待!

闲言少叙,让我们即刻启航!

Let‘s go!!!;)


isolated 关键字

Actor 从本质上来说就是一个同步器,它必须严格限制单个实例执行上下文以满足同步的语义。

这意味着在 Actor 中,所有可变属性、计算属性以及方法等默认都是被隔离执行的。

actor Foo {
    let name: String
    let age: Int
    var luck = 0
    
    init(name: String, age: Int, luck: Int = 0) {
        self.name = name
        self.age = age
        self.luck = luck
    }
    
    func incLuck() {
        luck += 1
    }
    
    var fullDesc: String {
        "\(name)[\(age)] luck is *\(luck)*"
    }
}

如上代码所示,Foo 中的 luck 可变属性、incLuck 方法以及 fullDesc 计算属性默认都被打上了 isolated 烙印。大家可以想象它们前面都隐式被 isolated 关键字修饰着,但这不能写出来,如果写出来就会报错:

在这里插入图片描述

在实际访问或调用这些属性或方法时,必须使用 await 关键字:

Task {
    let foo = Foo(name: "hopy", age: 11)
    await foo.incLuck()
    print(await foo.luck)
    print(await foo.fullDesc)
}

正是 await 关键字为 Foo 实例内容的同步创造了隔离条件,以摧枯拉朽之势将数据竞争覆巢毁卵。

nonisolated 关键字

但是在有些情况下 isolated 未免有些“防御过度”了。

比如,如果我们希望 Foo 支持 CustomStringConvertible 协议,那么势必需要实现 description 属性:

extension Foo: CustomStringConvertible {
    var description: String {
        "\(name)[\(age)]"
    }
}

如果大家像上面这样写,那将会妥妥的报错:

在这里插入图片描述

因为 description 作为计算属性放在 Actor 中,其本身默认处在“隔离”状态,而 CustomStringConvertible 对应的 description 实现必须是“非隔离”状态!

大家可以这样理解:我们不能异步调用 foo.description!

extension Foo: CustomStringConvertible {
    /*
    var description: String {
        "\(name)[\(age)]"
    }*/
    
    var fakeDescription: String {
        "\(name)[\(age)]"
    }
}

Task {
    let foo = Foo(name: "hopy", age: 11)
    // foo.description 不能异步执行!!!
    print(await foo.fakeDescription)
}

大家或许注意到,在 Foo#description 中,我们只使用了 Foo 中的只读属性。因为 Actor 中只读属性都是 nonisolated 隐式修饰,所以这时我们可以显式用 nonisolated 关键字修饰 description 属性,向 Swift 表明无需考虑 Foo#description 计算属性内部的同步问题,因为里面没有任何可变的内容:

extension Foo: CustomStringConvertible {
    nonisolated var description: String {
        "\(name)[\(age)]"
    }
}

Task {
    let foo = Foo(name: "hopy", age: 11)
    print(foo)
}

但是,如果 nonisolated 修饰的计算属性中含有可变(isolated)内容,还是会让编译器“怨声载道”:

在这里插入图片描述

没有被 async 修饰的方法也可以被异步等待!

最后,我们再介绍 isolated 关键字一个非常有用的使用场景。

考虑下面的 incLuck() 全局函数,它负责递增传入 Foo 实例的 luck 值,由于 Actor 同步保护“魔法”的存在,它必须是一个异步函数:

func incLuck(_ foo: Foo) async {
    await foo.incLuck()
}

不过,如果我们能够保证 incLuck() 方法传入 Foo 实参的“隔离性”,则可以直接访问其内部的“隔离”(可变)属性!

如何保证呢?

很简单,使用 isolated 关键字:

func incLuck2(_ foo: isolated Foo) {
    foo.luck += 1
}

看到了吗? luck 是 Foo 内部的“隔离”属性,但我们竟然可以在外部对其进行修改,是不是很神奇呢?

这里,虽然 incLuck2() 未用 async 修饰,但它仍是一个异步方法,我称之为全局“隐式异步”方法:

Task {
    let foo = Foo(name: "hopy", age: 11)
    await incLuck(foo)
    await incLuck2(foo)
}

虽然 foo 是一个 Actor 实例,它包含一些外部无法直接查看的“隔离”内容,但我们仍然可以使用一些调试手段探查其内部,比如 dump 方法:

Task {
    let foo = Foo(name: "hopy", age: 11)
    await incLuck(foo)
    await incLuck2(foo)
    dump(foo)
}

输出如下:

over
hopy[11]
▿ hopy[11] #0
  - $defaultActor: (Opaque Value)
  - name: "hopy"
  - age: 11
  - luck: 2

通过 dump() 方法输出可以看到,foo 的 luck 值被正确增加了 2 次,棒棒哒!!!💯

总结

在本篇博文中,我们通过几个通俗易懂的例子让小伙伴们轻松了解到 Swift 新 async/await 并发模型中 isolated 与 nonisolated 关键字的精髓,并对它们做了进一步的深入拓展。

感谢观赏,再会!8-)

如何让异步序列(AsyncSequence)优雅的感知被取消(Cancel)

在这里插入图片描述

概览

自  从 Swift 5.5 推出新的 async/await 并发模型以来,异步队列(AsyncSequence)就成为其中不可或缺的重要一员。

不同于普通的序列,异步序列有着特殊的“惰性”和并发性,若序列中的元素还未准备好,系统在耐心等待的同时,还将宝贵的线程资源供其它任务去使用,极大的提高了系统整体性能。

在本篇博文中,您将学到以下知识:

  1. 什么是异步序列?
  2. 创建自定义异步序列
  3. 另一种创建异步序列的方式:AsyncStream
  4. 取消异步序列的处理

什么是异步序列?

异步序列(AsyncSequence)严格的说是一个协议,它为遵守者提供异步的、序列的、迭代的序列元素访问。

在这里插入图片描述

表面看起来它是序列,实际上它内部元素是异步产生的,这意味着当子元素暂不可用时使用者将会等待直到它们可用为止: 在这里插入图片描述

诸多系统框架都对异步序列做了相应扩展,比如 Foundation 的 URL、 Combine 的发布器等等:

// Foundation
let url = URL(string: "https://kinds.blog.csdn.net/article/details/132787577")!
Task {
    do {
        for try await line in url.lines {
            print(line)
        }
    }catch{
        print("ERR: \(error.localizedDescription)")
    }
}

// Combine
let p = PassthroughSubject<Int,Never>()
for await val in p.values {
    print(val)
}

如上代码所示,URL#lines 和 Publisher#values 属性都是异步序列。

除了系统已为我们考虑的以外,我们自己同样可以非常方便的创建自定义异步序列。

创建自定义异步序列

一般来说,要创建自定义异步序列我们只需遵守 AsyncSequence 和 AsyncIteratorProtocol 协议即可:

在这里插入图片描述

下面我们就来创建一个“超级英雄们(Super Heros)”的异步序列吧:

struct SuperHeros: AsyncSequence, AsyncIteratorProtocol {
    
    private let heros = ["超人", "钢铁侠", "孙悟空", "元始天尊", "菩提老祖"]
    
    typealias Element = String
    var index = 0
    
    mutating func next() async throws -> Element? {
        defer { index += 1}
        
        try? await Task.sleep(for: .seconds(1.0))
        
        if index >= heros.count {
            return nil
        }else{
            return heros[index]
        }
    }
    
    func makeAsyncIterator() -> SuperHeros {
        self
    }
}

Task {
    let heros = SuperHeros()
    for try await hero in heros {
        print("出场英雄:\(hero)")
    }
}

以上异步序列会每隔 1 秒“产出”一名超级英雄:

在这里插入图片描述

如上代码所示,如果下一个超级英雄还未就绪,系统会在等待同时去执行其它合适的任务,不会有任何资源上的浪费。

另一种创建异步序列的方式:AsyncStream

其实,除了直接遵守 AsyncSequence 协议以外,我们还有另外一种选择:AsyncStream!

不像 AsyncSequence 和 AsyncIteratorProtocol 协议 ,AsyncStream 是彻头彻尾的结构(实体): 在这里插入图片描述

它提供两种构造器,分别供正常和异步序列产出(Spawning)情境使用:

public init(_ elementType: Element.Type = Element.self, bufferingPolicy limit: AsyncStream<Element>.Continuation.BufferingPolicy = .unbounded, _ build: (AsyncStream<Element>.Continuation) -> Void)

    
public init(unfolding produce: @escaping () async -> Element?, onCancel: (@Sendable () -> Void)? = nil)

下面为此举两个  官方提供的代码示例:

let stream_0 = AsyncStream<Int>(Int.self,
                    bufferingPolicy: .bufferingNewest(5)) { continuation in
     Task.detached {
         for _ in 0..<100 {
             await Task.sleep(1 * 1_000_000_000)
             continuation.yield(Int.random(in: 1...10))
         }
         continuation.finish()
    }
}

let stream_1 = AsyncStream<Int> {
    await Task.sleep(1 * 1_000_000_000)
    return Int.random(in: 1...10)
}

更多关于异步序列的知识,请小伙伴们移步如下链接观赏:


取消异步序列的处理

我们知道  新的 async/await 并发模型主打一个“结构化”,之所以称为“结构化”一个重要原因就是并发中所有任务都共同组成一个层级继承体系,当父任务出错或被取消时,所有子任务都会收到取消通知,异步序列同样也不例外。

就拿下面倒计时异步序列来说吧,它能感应父任务取消事件的原因是由于其中调用了 Task.sleep() 方法( sleep() 方法内部会对取消做出响应):

let countdown = AsyncStream<String> { continuation in
    Task {
        for i in (0...3).reversed() {
            try await Task.sleep(until: .now + .seconds(1.0), clock: .suspending)
            
            guard i > 0 else {
                continuation.yield(with: .success("🎉 " + "see you!!!"))
                return
            }
            
            continuation.yield("\(i) ...")
        }
    }
}

Task {
    for await count in countdown {
        print("current is \(count)")
    }
}

正常情况下,我们应该在异步序列计算昂贵元素之前显式检查 Cancel 状态:

let stream_1 = AsyncStream<Int> {
    // 假设 spawn() 是一个“昂贵”方法
    func spawn() -> Int {
        Int.random(in: 1...10)
    }
    
    // 或者使用 Task.checkCancellation() 处理异常
    if Task.isCancelled {
        return nil
    }
    
    return spawn()
}

在某些情况下,我们希望用自己的模型(Model)去关联退出状态,这时我们可以利用 withTaskCancellationHandler() 方法为异步序列保驾护航:

public func next() async -> Order? {
    return await withTaskCancellationHandler {
        let result = await kitchen.generateOrder()
        // 使用自定义模型中的状态来判断是否取消
        guard state.isRunning else {
            return nil
        }
        return result
    } onCancel: {
    // 在父任务取消时设置取消状态!
        state.cancel()
    }
}

注意,当父任务被取消时上面 onCancel() 闭包中的代码会立即执行,很可能和 withTaskCancellationHandler() 方法主体代码同步进行。

现在,在一些 Task 内置取消状态不适合或不及时的场合下,我们可以在异步序列中使用 withTaskCancellationHandler() 的 onCancel() 子句来更有效率的完成退出操作,棒棒哒!💯。

总结

在本篇博文中,我们首先简单介绍了什么是异步序列,接着学习了几种创建自定义异步序列的方法,最后我们讨论了如何优雅的取消异步序列的迭代。

感谢观赏,再会!8-)

Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出

在这里插入图片描述

0. 概览

Swift 5.9 一声炮响为我们带来全新的宏(Macro)机制,也同时带来了干霄凌云的 Observation 框架。

在这里插入图片描述

Observation 框架可以增强通用场景下的使用,也可以搭配 SwiftUI 5.0 而获得双剑合璧的更强威力。

在本篇博文,您将学到如下内容:

  1. @Observable 宏
  2. 通用情境下如何观察 Observable 对象?
  3. Observable 对象与 SwiftUI 珠联璧合
  4. 被“抛弃的” @EnvironmentObject
  5. 在视图中将不可变 Observable 对象转换为可变对象的妙招

那么,就让我们赶快进入 Observation 奇妙的世界吧!

Let‘s go!!!;)


1. @Observable 宏

简单来说,Observation 框架为我们提供了集鲁棒性(robust)、安全性、高性能等三大特性为一身的 Swift 全新观察者设计模式。

它的核心功能在于:监视对象状态,并在改变时做出反应!

在这里插入图片描述

在 Swift 5.9 中,我们可以非常轻松的通过 @Observable 宏将普通类“转化为”可观察(Observable)类。自然,它们的实例都是可观察的:

@Observable
final class Hero {
    var name: String
    var power: Int
    
    init(name: String, power: Int) {
        self.name = name
        self.power = power
    }
}

@Observable
final class Model {
    var title: String
    var createAt: Date?
    var heros: [Hero]
    
    init(title: String, heros: [Hero]) {
        self.title = title
        self.createAt = Date.now
        self.heros = heros
    }
}

如上代码所示,我们定义了两个可观察类 Model 和 Hero,就是这么简单!

2. 通用情境下如何观察 Observable 对象?

在一个对象成为可观察之后,我们可以通过 withObservationTracking() 方法随时监听它状态的改变:

在这里插入图片描述

我们可以将对象需要监听的属性放在 withObservationTracking() 的 apply 闭包中,当且仅当( Hero 中其它属性的改变不予理会)这些属性发生改变时其 onChange 闭包将会被调用:

let hero = Hero(name: "大熊猫侯佩", power: 5)

func watching() {
    withObservationTracking({
        NSLog("力量参考值:\(hero.power)")
    }, onChange: {
        NSLog("改变之前的力量!:\(hero.power)")
        watching()
    })
}

watching()

hero.name = "地球熊猫"
hero.power = 11
hero.power = 121

以上代码输出如下:

在这里插入图片描述

使用 withObservationTracking() 方法有 3 点需要注意:

  1. 它默认只会被调用 1 次,所以上面为了能够重复监听,我们在 onChange 闭包里对 watching() 方法再次进行了调用;
  2. withObservationTracking() 方法的 apply 闭包不管如何都会被调用 1 次,即使其监听的属性从未改变过;
  3. 在监听闭包中只能得到属性改变前的旧值;

目前,上面测试代码在 Xcode 15 的 Playground 中编译会报错,提示如下:

error: test15.playground:8:13: error: external macro implementation type 'ObservationMacros.ObservableMacro' could not be found for macro 'Observable()' final class Hero { ^

Observation.Observable:2:180: note: 'Observable()' declared here @attached(member, names: named(_$observationRegistrar), named(access), named(withMutation)) @attached(memberAttribute) @attached(extension, conformances: Observable) public macro Observable() = #externalMacro(module: "ObservationMacros", type: "ObservableMacro")

小伙伴们可以把它们放在 Xcode 的 Command Line Tool 项目中进行测试:

在这里插入图片描述


3. Observable 对象与 SwiftUI 珠联璧合

要想发挥 Observable 对象的最大威力,我们需要 SwiftUI 来一拍即合。

在 SwiftUI 中,我们无需再显式调用 withObservationTracking() 方法来监听改变,如虎添翼的 SwiftUI 已为我们自动完成了所有这一切!

struct ContentView: View {
    let model = Model(title: "地球超级英雄", heros: [])

    var body: some View {        
        NavigationStack {
            Form {
                LabeledContent(content: {
                    Text(model.title)
                }, label: {
                    Text("藏匿点名称")
                })
                
                LabeledContent(content: {
                    Text(model.createAt?.formatted(date: .omitted, time: .standard) ?? "无")
                }, label: {
                    Text("更新时间")
                })
                
                Button("刷新") {
                    // SwiftUI 会自动监听可观察对象的改变,并刷新界面
                    model.title = "爱丽丝仙境兔子洞"
                    model.createAt = Date.now
                }
            }.navigationTitle(model.title)
        }
    }
}

注意,上面代码中 model 属性只是一个普通的 let 常量,即便如此 model 的改变仍会反映到界面上:

在这里插入图片描述

4. 被“抛弃的” @EnvironmentObject

有了 Swift 5.9 中新 Observation 框架加入游戏,在 SwiftUI 5.0 中 EnvironmentObject 再无用武之地,我们仅用 Environment 即可搞定一切!

早在 SwiftUI 1.0 版本时,其就已经提供了 Environment 对应的构造器:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {...}

有了新 Observation 框架的入驻,结合其 Observable 可观察对象,Environment 可以再次大放异彩:

struct HeroListView: View {
    @Environment(Model.self) var model
    
    var body: some View {
        List(model.heros) { hero in
            HStack {
                Text(hero.name)
                    .font(.headline)
                Spacer()
                Text("\(hero.power)")
                    .font(.subheadline)
                    .foregroundStyle(.gray)
            }
        }
    }
}

struct ContentView: View {
    @State var model = Model(title: "地球超级英雄", heros: [
        .init(name: "大熊猫侯佩", power: 5),
        .init(name: "孙悟空", power: 1000),
        .init(name: "哪吒", power: 511)
    ])

    var body: some View {        
        NavigationStack {
            Form {
                NavigationLink(destination: HeroListView().environment(model)) {
                    Text("查看所有英雄")
                }
            }.navigationTitle(model.title)
        }
    }
}

现在,即使跨越多重层级关系我们也可以只通过 @Environment 而不用 @EnvironmentObject 来完成状态的间接传递了,是不是很赞呢?👍🏻

5. 在视图中将不可变 Observable 对象转换为可变对象的妙招

介绍了以上这许多,就还剩一个主题没有涉及:Observable 对象的可变性!

为了能够在子视图中更改对应的可观察对象,我们可以用 @Bindable 修饰传入的 Observable 对象:

struct HeroView: View {
    @Bindable var hero: Hero
    
    var body: some View {
        Form {
            TextField("名称", text: $hero.name)
            
            TextField("力量", text: .init(get: {
                String(hero.power) 
            }, set: {
                hero.power = Int($0) ?? 0
            }))
        }
    }
}

不过,对于之前 @Environment 那个例子来说,如何达到子视图能够修改传入的 @Environment 可观察对象呢?

别急,我们可以利用称为“临时可变(Temporary Variable)”的技术将原先不可变的可观察对象改为可变:

extension Hero: Identifiable {
    var id: String {
        name
    }
}

struct HeroListView: View {
    @Environment(Model.self) var model
    
    var body: some View {
        // 在 body 内将 model 改为可变
        @Bindable var model = model
        
        VStack {
            List(model.heros) { hero in
                HStack {
                    Text(hero.name)
                        .font(.headline)
                    Spacer()
                    Text("\(hero.power)")
                        .font(.subheadline)
                        .foregroundStyle(.gray)
                }
            }.safeAreaInset(edge: .bottom) {
            // 绑定可变 model 中的状态以修改英雄名称
                TextField("", text: $model.heros[0].name)
                    .padding()
            } 
        }
    }
}

运行效果如下:

在这里插入图片描述

“临时可变”这一技术可以用于视图中任何化“不变”为“可变”的场景中,当然对于直接视图间对象的传递,我们可以使用 @Bindable 这一更为“正统”的方法。

6. 总结

在本篇博文中,我们讨论了在 Swift 5.0 和 SwiftUI 5.0 中大放异彩 Observation 框架的使用,并就诸多技术细节问题给与了详细的介绍,愿君喜欢。

感谢观赏,再会!8-)

SwiftUI 4.0:两种方式实现子视图导航功能

在这里插入图片描述

0. 概览

从 SwiftUI 4.0 开始,觉悟了的苹果毅然抛弃了已“药石无效”的 NavigationView,改为使用全新的 NavigationStack 视图。

诚然,NavigationStack 从先进性来说比 NavigationView 有不小的提升,若要如数家珍得单开洋洋洒洒的一篇来介绍。


关于 SwiftUI 中旧 NavigationView 视图种种人神共愤的“诟病”和弊端,请移步我的其它专题博文观赏:


不过,这些都不是本篇的主旨。在本篇中我们将尝试一起另辟蹊径来完成两种截然不同的导航机制。

在本篇博文,您将学到如下内容:

  1. NavigationStack
  2. NavigationSplitView 导航之“假象”
  3. 洞若观火:在 iPad 上的比较

无需等待,Let’s go!!!;)


1. NavigationStack

从 SwiftUI 4.0 开始, 引入的新 NavigationStack 导航器终于不再以分散杂乱的数据作为导航触发媒介,而是将有序的数据集合作为导航跳转的核心来对待!(所以,一步跳回根视图成了雕虫小技)

其中,NavigationStack 导航器重要的组成部分 NavigationLink 除了为了兼容性暂时保留的传统跳转方式以外,主打以状态本身的值作为导航的基石。这样,对于不同类型状态触发的跳转,我们可以干净而从容的分别处理:

下面举一例。

首先,定义简单的数据结构,Alliance 中包含若干 Hero:

@Observable
final class Hero {
    var name: String
    var power: Int
    
    init(name: String, power: Int) {
        self.name = name
        self.power = power
    }
}

extension Hero: Identifiable {
    var id: String {
        name
    }
}

extension Hero: Hashable {
    static func == (lhs: Hero, rhs: Hero) -> Bool {
        lhs.name == rhs.name && lhs.power == rhs.power
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(power)
    }
}


@Observable
final class Alliance: Hashable {
    static func == (lhs: Alliance, rhs: Alliance) -> Bool {
        lhs.title == rhs.title
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
    }
    
    var title: String
    var createAt: Date?
    var heros: [Hero]
    
    init(title: String, heros: [Hero]) {
        self.title = title
        self.createAt = Date.now
        self.heros = heros
    }
}

接下来是与之对应的子视图,注意驱动 NavigationLink 导航的是 Hero 本身,而不是什么“莫名奇妙”的条件变量:

struct HeroDetailView: View {
    let hero: Hero
    
    var body: some View {
        VStack {
            Text("力量: \(hero.power)")
                .font(.largeTitle)
            
        }.navigationTitle(hero.name)
    }
}

struct HeroListView: View {
    @Environment(Alliance.self) var model
    
    var body: some View {        
        VStack {
            List(model.heros) { hero in
                NavigationLink(value: hero) {
                    HStack {
                        Text(hero.name)
                            .font(.headline)
                        Spacer()
                        Text("\(hero.power)")
                            .font(.subheadline)
                            .foregroundStyle(.gray)
                    }
                }
            }
        }
    }
}

接着是主视图:

struct ContentView: View {
    @State var model = Alliance(title: "地球超级英雄", heros: [
        .init(name: "大熊猫侯佩", power: 5),
        .init(name: "孙悟空", power: 1000),
        .init(name: "哪吒", power: 511)
    ])

    var body: some View {
        NavigationStack {
            Form {
                NavigationLink("查看所有英雄", value: model)
            }
            .navigationDestination(for: Alliance.self) { model in
                HeroListView()
                    .environment(model)
            }
            .navigationDestination(for: Hero.self) { hero in
                HeroDetailView(hero: hero)
            }
            .navigationTitle(model.title)
        }
    }
}

从上面源代码中,我们可以看到几处有趣的地方:

  1. 子视图 HeroListView 和主视图 ContentView 都包含了 NavigationLink,但它们驱动状态的类型不一样(分别是 Hero 和 Model),这样不同的驱动源被清晰的区分开了;
  2. 放置 NavigationLink 和实际发生导航跳转的目标位置是分开的(通过 navigationDestination() 修改器),后者被放在了一起便于集中管理;

正是这些新的导航特性确保了导航逻辑代码清楚且集中,为日后自己或其它秃头码农来维护打下夯实基础:

在这里插入图片描述

以上就是第一种导航方法,即利用 NavigationStack + navigationDestination() 修改器方法来合作完成跳转功能。

2. NavigationSplitView 导航之“假象”

可能有的小伙伴们没太在意,SwiftUI 4.0 除了 NavigationStack 以外还新加入了另一个鲜为人知的导航器 NavigationSplitView!使用它,我们可以抛弃 navigationDestination() 去实现完全相同的导航功能。

我们对之前代码略作修改,看看能促成什么新奇的“玩法”:

struct HeroListView: View {
    @Environment(Alliance.self) var model
    @Binding var selection: Hero?
    
    var body: some View {        
        VStack {
            List(model.heros, selection: $selection) { hero in
                NavigationLink(value: hero) {
                    HStack {
                        Text(hero.name)
                            .font(.headline)
                        Spacer()
                        Text("\(hero.power)")
                            .font(.subheadline)
                            .foregroundStyle(.gray)
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @State var model = Alliance(title: "地球超级英雄", heros: [
        .init(name: "大熊猫侯佩", power: 5),
        .init(name: "孙悟空", power: 1000),
        .init(name: "哪吒", power: 511)
    ])
    
    @State private var selection: Hero?

    var body: some View {
        NavigationSplitView(sidebar: {
            HeroListView(selection: $selection)
                .environment(model)
                .navigationTitle("新导航方式")
        }, detail: {
            if let selection {
                HeroDetailView(hero: selection)
            } else {
                ContentUnavailableView("No Hero", systemImage: "person.badge.key.fill", description: Text("还未选中任何英雄!"))

            }
        })
    }
}

可以看到,修改后的代码与之前有几处不同:

  1. 使用 NavigationSplitView 而不是 NavigationStack;
  2. 没有使用任何 navigationDestination() 修改器方法;
  3. 向 List 构造器传入了 selection 参数,以判断用户选择了哪个 Hero;
  4. 根据 selection 的值驱动 NavigationSplitView 构造器 detail 闭包完成跳转功能;

简单来说:当用户选中任意 Hero 后,通过设置 NavigationSplitView 构造器 detail 闭包中的视图,我们完成了导航机制。

代码执行结果和之前几乎完全相同,这么神奇!?

可惜,你们看到的全是“假象”!!!

3. 洞若观火:在 iPad 上的比较

其实,设置 NavigationSplitView 构造器 detail 闭包的内容原本并不会造成导航跳转,它的原意是在大屏设备上更方便的利用大尺寸屏幕来浏览内容。

编译上面 NavigationSplitView 的实现,在 iPad 上运行看看效果:

在这里插入图片描述

看到了吗?第二种导航机制在大屏设备上原本并不是真正用来导航跳转的,只是在 iPhone 等小屏设备上它的行为退化成了导航!

而第一种导航实现是彻头彻尾、如假包换的“真”导航:

在这里插入图片描述

到底哪种方法更好一些?小伙伴们自有“仁者见仁智者见智”的看法,欢迎大家随后的讨论。

至此,我们完成了文章开头的目标,棒棒哒!!!💯

4. 总结

在本篇博文中,我们在 SwiftUI 4.0 里通过两种不同方式实现了相同的子视图导航功能,任君选择。

感谢观赏,再会!8-)

Swift 和 Python 两种语言中带关联信息错误(异常)类型的比较

在这里插入图片描述

0. 概览

如果我们分别在平静如水、和谐感人的 Swift 和 Python 社区抛出诸如“Python 是天下最好的语言...” 和 “Swift 是宇宙第一语言...”之类的言论会有怎样的“下场”?

我们并不想对可能发生的“炸裂”景象做出什么预测,也无意比较 Swift 与 Python 的孰强孰弱。我们只是想聊聊 Swift 和 Python 语言中一个非常有趣的话题:看看谁实现带关联信息的错误(异常)类型更优雅?

在本篇博文中,您将了解到如下内容:

  1. “抛出?还是不抛出错误?这是个问题!”
  2. 带关联信息的错误
  3. Python 中的实现
  4. Swift 中实现
  5. 那么问题来了:谁更优雅呢?

还等什么呢?Let’s go!!!;)


1. “抛出?还是不抛出错误?这是个问题!”

在秃头码农们辛勤操劳的每一天都会遇到多如牛毛的代码错误,俗话说得好:“事无常形,人无完人”,没有错误的代码只能是还没有写出来的代码。

所幸的是几乎所有语言均对错误做出了良好和友善的支持,Swift 与 Python 自然也不例外!

在 Swift 中我们把代码能够抛出的某种“不安分”的东东称为错误(Error),而在 Python 中这被称之为异常(Exception)。其实从这种意义上来说它们是同一种东西,所以在本篇博文中统统称之为错误

当函数或方法出现“不按常理出牌”的结果时,我们就有必要抛出一个错误。比如除以零:

enum AppError: Error {
    case divisionByZero
}

func div(dividend: Int, divisor: Int) throws -> Int {
    guard divisor != 0 else {
        throw AppError.divisionByZero
    }
    
    return dividend / divisor
}

上面是 Swift 中对此的一个实现,在 Python 中有类似的表达:

class DivisionByZero(Exception):
    pass

def div(dividend, divisor):
    if divisor == 0:
        raise DivisionByZero()
    
    return dividend / divisor

2. 带关联信息的错误

在某些情况下,我们希望抛出的错误中携带一些有用的信息,这种错误称之为:带关联信息的错误

比如当用户输入的文本太短不符合要求时,我们希望进一步了解到用户输入文本的实际长度以及最短需要的长度。

在 Swift 中我们可以通过枚举关联类型来实现带关联信息的错误,类似的在 Python 中我们可以用异常类的附加实例属性来达到这一相同目的。


关于更多 Swift 中枚举使用的奥秘,请小伙伴们移步我的专题视频观看:

如果小伙伴们还想进一步学习 Swift 语言的开发技巧,可以到我的专题专栏中继续研究:


下面我们就来分别看看它们的实现吧!

3. Python 中的实现

在这里插入图片描述

在 Python 中可以简单的为自定义异常类增加实例属性变量:

class TextTooShortException(Exception):
    def __init__(self, actual, min):
        super().__init__(self)
        self.actual = actual
        self.min = min

MIN_LEN = 3

try:
    text = "wo"
    if len(text) < MIN_LEN:
        raise TextTooShortException(len(text), MIN_LEN)
except TextTooShortException as error:
    print("错误:文本太短({}),希望不小于 {} 个字符!".format(len(text), MIN_LEN))

如上代码所示,我们创建了一个自定义异常类 TextTooShortException,其中分别用 actual 和 min 实例属性变量来存放输入文本实际和最短所需的长度。

我们看看运行结果:

在这里插入图片描述

4. Swift 中实现

在这里插入图片描述

在 Swift 中对于错误这种不需要实例化的对象,一般可以用枚举来表示(当然也可以用 struct,这要分情况而定)。

对于带关联信息的错误,我们只需要创建带关联类型的枚举元素即可:

enum MyError: Error {
    case textTooShort(actual: Int, min: Int)
}

let text = "wo"

do {
    if text.count < MIN_LEN {
        throw MyError.textTooShort(actual: text.count, min: MIN_LEN)
    }
} catch let my_error as MyError {
    if case MyError.textTooShort(let actual, let min) = my_error {
        print("输入文字长度(\(actual))太短了,不能小于(\(min))")
    } else {
        print("其它 MyError: \(my_error)")
    }
} catch {
    print("Other ERR: \(error)")
}

在上面的代码中,我们创建了一个 MyError 类型的错误,并添加了一个 textTooShort 枚举子项。其中我们为其嵌入了两个 Int 值作为关联类型,分别用来表示 actual 和 min 值。

以下是 Playground 中的运行结果:

在这里插入图片描述

5. 那么问题来了:谁更优雅呢?

看过上面两种实现,各位小伙伴们可能都会有自己的考量。

在 Swift 中使用类似于命名元组的枚举关联类型,显得更轻量级。表达 Error 这一概念无需动用 Class 这一重型武器,一个简单的值类型(enum)足以!

在 Python 中的实现也很简单,不过使用类来作为异常的载体显得更加“厚重”,更加中规中矩。

平心而论:Python 在捕获并处理异常时更加简洁,而 Swift 在定义错误时更轻量级,可惜关联类型枚举在错误解析时比较拖垮。

不过,谁说在 Swift 中不能用 Python 的方式自定义错误呢?;)

struct TextTooShortError: Error {
    var actual: Int
    var min: Int
}

let text = "wo"

do {
    if text.count < MIN_LEN {
        throw TextTooShortError(actual: text.count, min: MIN_LEN)
    }
} catch let tooShortError as TextTooShortError {
    print("输入文字长度(\(tooShortError.actual))太短了,不能小于(\(tooShortError.min))")
} catch {
    print("Other ERR: \(error)")
}

总结

在本篇博文中,我们讨论了在 Swift 和 Python 两种语言中对于带关联信息的错误(异常)类型是如何实现的这一话题,并对哪种实现更优雅给出笔者自己的感悟。

感谢观赏,再会!8-)

Swift 抛砖引玉:从数组访问越界想到的“可抛出错误”属性

在这里插入图片描述

0. 概览

了解 Swift 语言的小伙伴们都知道,我们可以很方便的写一个可能抛出错误的方法。不过大家可能不知道的是在 Swift 中对于结构或类的实例属性我们也可以让它抛出错误。

这称之为实效只读属性(Effectful Read-only Properties)。

那么,这种属性怎么创建?并且到底有什么用处呢?

在本篇博文中,您将学到如下内容:

  1. 什么是“实效只读属性”
  2. 怎么创建“实效只读属性”?
  3. 数组访问越界是个头疼的问题
  4. 拯救者:抛出错误的“实效只读属性”
  5. 更进一步
  6. 八卦一下:ruby 中更优雅的实现

相信看完文本后,小伙伴们的武器库中又会多了一件“杀手锏”!

那还等什么呢?Let‘s go!!!;)


1. 什么是“实效只读属性”

“实效只读属性” 英文名称为 Effectful Read-only Properties,它是 Swift 5.5+ 中对计算属性和下标操作(computed properties and subscripts)的增强功能。

在 Swift 5.5 之前,我们只能创建异步或可抛出错误的方法(或函数),而无法构建与此类似的实例属性。

对于有些情况,一个“异步”属性可以帮上大忙!

actor AccountManager {
  // 注意: `getLastTransaction` 方法若在 AccountManager 外部调用将会“升级”为一个异步方法
  func getLastTransaction() -> Transaction { /* ... */ }
  func getTransactions(onDay: Date) async -> [Transaction] { /* ... */ }
}

class BankAccount {
  
  private let manager: AccountManager?
  var lastTransaction: Transaction {
    get {
      guard let manager = manager else {
         throw BankError.NoManager
      // ^~~~~ 错误: 普通计算属性中不能抛出错误!
      }
      return await manager.getLastTransaction()
      //     ^~~~~ 错误: 普通计算属性中不能调用异步方法
    }
  }
}

如上代码所示:在 BankAccount 类的 lastTransaction 实例属性访问过程中可能会抛出错误,并且需要等待返回一个异步方法的结果。这对于以往的实例属性来说是“不可能的任务”!

诚然,我们可以将 lastTransaction 实例属性变为一个方法:

class BankAccount {
  private let manager: AccountManager?
  //var lastTransaction: Transaction {}

  func getLastTransaction() async throws -> Transaction {
    guard let manager = manager else {
         throw BankError.NoManager
      }
      return await manager.getLastTransaction()
  }
}

但这显然有点“画蛇添足”的意味。

幸运的是, 倾听到了秃头码农们的殷切呼唤,从 Swift 5.5 开始我们便有了上面的“实效只读属性”。


想进一步了解“实效只读属性”的小伙伴们可以到 Swift 语言进化提案(swift-evolution proposals)中观赏更详细的内容:


2. 怎么创建“实效只读属性”?

从 Swift 5.5+ 开始,我们可以在实例属性的只读访问器(get)上应用 async 或 throws 关键字(效果说明符):

class BankAccount {
  // ...
  var lastTransaction: Transaction {
    get async throws {   // <-- Swift 5.5+: 效果说明符(effects specifiers)!
      guard manager != nil else {
        throw BankError.notInYourFavor
      }
      return await manager!.getLastTransaction()
    }
  }

  subscript(_ day: Date) -> [Transaction] {
    get async { // <-- Swift 5.5+: 与上面类似,我们也可以在下标的读操作上应用效果说明符。
      return await manager?.getTransactions(onDay: day) ?? []
    }
  }
}

如上代码所示,我们不但可以在实例属性上应用 async 和 throws 效果说明符(effects specifiers),同样也可以在类或结构下标操作的读访问器上使用它们。

现在,我们可以这样访问 BackAccount#lastTransaction 实例属性和下标操作:

extension BankAccount {
  func meetsTransactionLimit(_ limit: Amount) async -> Bool {
    return try! await self.lastTransaction.amount < limit
    //                    ^~~~~~~~~~~~~~~~
    //                    对该实例属性的访问是异步且可能抛出错误的!
  }                
}

func hadWithdrawlOn(_ day: Date, from acct: BankAccount) async -> Bool {
  return await !acct[day].allSatisfy { $0.amount >= Amount.zero }
  //            ^~~~~~~~~
  //            同样的,下标读操作也是异步的
}

3. 数组访问越界是个头疼的问题

秃头码农们都知道,在 Swift 中对于数组访问常常出现下标越界的情况。它会引起程序立即崩溃!

我们时常会想:如果在数组访问越界时抛出一个可捕获的错误就好了!

在过去,我们可以写一个新的“下标访问”方法来模拟这一“良好愿望”:

enum Error: Swift.Error {
    case outOfRange
}

extension Array where Element: Equatable {
    func getElemenet(at: Array.Index) throws -> Element {
        guard at < endIndex else {
            throw Error.outOfRange
        }
        
        return self[at]
    }
}

do {
    let ary = Array(1...100)
    _ = try ary.getElemenet(at: 10000)
} catch let error as Error {
    print("ERR: \(error.localizedDescription)")
}

但这种 .getElemenet(at:) 的“丑陋”写法真是让人“是可忍孰不可忍”!

不过,从 Swift 5.5 一切开始变得不同了。

4. 拯救者:抛出错误的“实效只读属性”

看到这,聪明的小伙伴们应该早就知道如何应对了。

我们可以使用 Swift 5.5 中的“实效只读属性”来“完美的”完成任务:

enum Error: Swift.Error {
    case outOfRange
}

extension Array where Element: Equatable {
    subscript(index: Array.Index) -> Element {
        get throws {
            guard index < endIndex else {
                throw Error.outOfRange
            }
            
            var temp = self
            temp.swapAt(0, index)
            return temp.first!
        }
    }
}

do {
    let ary = Array(1...100)
    _ = try ary[10000]
} catch let error as Error {
    print("ERR: \(error.localizedDescription)")
}

如上代码所示:我们使用可抛出错误的下标读访问器为 Array 下标操作“添妆加彩”。略微遗憾的是,我们需要在数组新下标操作中调用原来的下标操作,这对于结构(struct)类型的 Array 来说好似“难于上青天”,所以我们采用的是迂回战术。


对于类支持的类型来说,我们可以使用 Objc 存在的 Swizz 技术来得偿所愿。


在文章最后,我们将会看到同样问题在 ruby 语言中实现的是何其优雅。

5. 更进一步

在数组的下标访问中抛出错误还不算完,我们还可以利用 Swift 枚举的关联类型为错误添加进一步的信息。


想要了解更多 Swift 枚举的小秘密,请小伙伴们移步如下文章观赏:

更多 Swift 语言知识的系统介绍,请移步我的专栏文章进一步观看:


下面,我们为之前的越界错误增加关联类型,分别表示当前越界的索引和数组总长度:

enum Error: Swift.Error {
    case outOfRange(accessing: Int, end: Int)
}

接着,修改抛出错误处的代码:

subscript(index: Array.Index) -> Element {
    get throws {
        guard index < endIndex else {
            throw Error.outOfRange(accessing: index, end: count)
        }
        
        var temp = self
        temp.swapAt(0, index)
        return temp.first!
    }
}

最后,是错误捕获时的代码:

do {
    let ary = Array(1...100)
    _ = try ary[10000]
} catch let error as Error {
    if case Error.outOfRange(let accessing, let end) = error {
        print("ERR: 数组访问越界[试图访问:\(accessing),数组末尾:\(end)]")
    }
}

现在,当发生越界错误时我们可以清楚的知道事情的来龙去脉了,是不是了很赞呢:

在这里插入图片描述

6. 八卦一下:ruby 中更优雅的实现

上面我们提到过 Swift 结构类型的方法“重载”(结构没有重载之说,这里只是比喻)无法再使用“重载”前的方法了。

但是在某些动态语言中,我们可以非常方便的使用类似于“钩子”机制来访问旧方法,比如 ruby 里:

#!/usr/bin/ruby

class Array
    alias :subscript :[]

    def [](index)
        puts "试图访问索引:#{index}"
        subscript(index)
    end
end

a = [1,2,3]
puts a[1]

如上所示,我们使用别名(alias)机制将原下标操作方法 :[] 用 :subscript 名称进行“缓存”,然后在新的 :[] 方法中我们可以直接调用旧方法。

运行结果如下所示:

试图访问索引:1
2

Swift 什么时候有这种“神奇”的能力呢?让我们翘首以盼!

总结

在本篇博文中,我们讨论了 Swift 5.5 中新增的“实效只读属性”(Effectful Read-only Properties),它有哪些用途?怎么用它来解决 Swift 数组访问越界的“老问题”?最后,我们用 ruby 代码举了一个更优雅的实现。

感谢观赏,再会!8-)

消失的它:摆脱 SwiftUI 中“嵌入视图数量不能超过 10 个”限制的秘密

在这里插入图片描述

概览

SwiftUI 带来了描述性界面布局的新玩法,让我们可以超轻松的创建复杂应用界面。但是在早期 SwiftUI 中有一个“著名”的限制大家知道么?那就是 @ViewBuilder 中嵌入子视图数量不能超过 10 个!

不过,从 Swift 5.9 开始这一“桎梏”已悄然消失的无影无踪。

在这里插入图片描述

这个限制为什么已然烟消云散?早期的限制又是如何产生的呢?

在本篇博文中,您将学到以下内容:

  1. 不能超过 10 个,你是来逗我的吗?
  2. “值与类型形参包”
  3. SwiftUI 的新实现
  4. 为何不能用泛型数组?

想知道事件的前因后果么?那还等什么呢?

Let‘s go!!!;)


1. 不能超过 10 个,你是来逗我的吗?

在 Swift 5.5 中增加了 some 关键字,让 SwiftUI 能够用简洁类型来描述海量复合视图。这还不算完,可能  觉得视图组合的手法还是太麻烦,随即又祭出 @ViewBuilder 来进一步简化 SwiftUI 的视图构建。


其实,SwiftUI 视图的 body 计算属性已被 @ViewBuilder 默默修饰着,我们能够轻松自在,全靠 @ViewBuilder 为我们负重前行:

@ViewBuilder var body: Self.Body { get }

更多 ViewBuilder 实现细节的讨论,请小伙伴们移步 Swift 官方社区观赏:


@ViewBuilder 其实是结果构建器(Result Builder,Swift 5.4)在 SwiftUI 中的一个实现。结果构建器可以被视为一种嵌入式领域特定语言(DSL),用于将收集的内容组合成最终的结果。

这就是我们可以这样创建 SwiftUI 复合视图的原因:

@ViewBuilder func lot(_ needDetails: Bool) -> some View {
    Text("Hello World")
        .font(.title)
    if needDetails {
        Text("大熊猫侯佩 @ csdn")
            .font(.headline)
            .foregroundStyle(.gray)
    }
}

在这里插入图片描述

那么,ViewBuilder 在内部是如何处理传入不定数量视图的呢?

ViewBuilder 为了满足 Result Builder 的语义,必须实现其规定的一系列方法:

在这里插入图片描述


取决于大家要实现 DSL 语言的完整性和复杂性,我们可以选择实现尽可能少或全部这些方法。

讨论如何用 Result Builder 来实现自己的 DSL 语言超出了本文的范畴,感兴趣的小伙伴们可以移步下面的链接观赏进一步内容:

想了解更多 Swift 语言开发的知识,小伙伴们可以到我的专题专栏中进行系统性学习:


而对于简单 View 的合成,ViewBuilder 竟然采用了一种最“蠢”的方式:为每种“可能”的情况手动定义一个方法。

于是乎,就有了下面这一大坨泛型方法:

在这里插入图片描述

正如小伙伴们所猜的那样,这些方法中最大可传入的参数数量就是 10 (c0-c9),所以这就是“桎梏”的根本原因:我们在 @ViewBuilder 中最多只能包含 10 个子视图。

static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View {
    return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}

对于超过 10 个视图的情况,我们只能“八仙过海各显神通”的尝试绕过它。

比如一种办法是:将 10 个以上的视图塞到多个 Group 中去。

2. “值与类型形参包”

从 Swift 5.9 开始,苹果似乎认识到之前的做法比较“二”,所以推出了新的“值与类型形参包”(Value and Type parameter packs)机制。

该机制专门用于处理不确定数量泛型参数的方法:

func eachFirst<each T: Collection>(_ item: repeat each T) -> (repeat (each T).Element?) {
    return (repeat (each item).first)
}

比如在上面代码中,我们用 each 和 repeat each 分别修饰了泛型参数的形参和结果部分。

eachFirst() 方法的作用是将所有传入集合的第一个元素组成一个新的元组。现在 eachFirst() 泛型方法可以接受任意个类型为 Collection 的参数,同时返回同样数量 Collection.Element? 类型元素的元组。

我们可以这样调用 eachFirst() 方法:

let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let firstValues = eachFirst(numbers, names)
print(firstValues) 
// (Optional(0), Optional("Antoine"))

看到了么?不管传入参数有多少个、不管它们是什么类型(至少必须是 Collection),eachFirst() 方法都可以正常工作。

有了“值与类型形参包”,我们处理泛型方法的灵活性提升一个新层级!

3. SwiftUI 的新实现

在 Swift 5.9 中,SwiftUI 用新“值与类型形参包”机制重写了 ViewBuilder 的实现。

不像之前每种情况“傻傻的”写一个对应的 buildBlock() 方法,现在只需一个带 each/repeat each 的 buildBlock() 方法足矣:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {

    public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
}

如上代码所示,我们现在可以向 @ViewBuilder 传递任意数量的视图了:

struct ContentView: View {
    var body: some View {
        Group {
            Text("1")
            Text("2")
            Text("3")
            Text("4")
            Text("5")
            Text("6")
            Text("7")
            Text("8")
            Text("9")
            Text("10")
            Text("11")
            Text("12")
        }
        .foregroundStyle(.white)
        .background {
            Circle()
                .fill(Color.blue.opacity(0.5))
                .frame(width: 35)
        }
        .shadow(radius: 5.0)
        .padding()
        .font(.title2.weight(.bold))
    }
}

是不是很赞呢?棒棒哒💯

4. 为何不能用泛型数组?

有些小伙伴可能觉得,为什么之前 eachFirst() 方法不能用泛型数组的方式来实现呢?用泛型数组不就可以传入任意数量的集合参数了吗?

我们来试一下:

func eachFirst<T: Collection>(collections: [T]) -> [T.Element?] {
    collections.map(\.first)
}

实际运行就会发现,如果用泛型数组则无法传入不同类型元素的集合:

在这里插入图片描述

这就是为什么上面代码报错的原因了。

有时候我们希望 eachFirst() 泛型方法中至少要带一个形参,这可以用类似下面的方式来实现:

func eachFirst<FirstT: Collection, each T: Collection>(_ firstItem: FirstT, _ item: repeat each T) -> (FirstT.Element?, repeat (each T).Element?) {
    return (firstItem.first, repeat (each item).first)
}

let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let booleans = [true, false, true]
let doubles = [3.3, 4.1, 5.6]

let firstValues = eachFirst(numbers, names, booleans, doubles)
print(firstValues) 
// (Optional(0), Optional("Antoine"), Optional(true), Optional(3.3))

总结

在本篇博文中,我们讨论了 SwiftUI 中“嵌入视图数量不能超过 10 个”这一限制的原因,并介绍了从 Swift 5.9+ 开始这一限制为什么最终消失了?

感谢观赏,再会!8-)

支付宝小程序IDE版本迭代异常

前情

uni-app是我比较喜欢的跨平台框架,它能开发小程序/H5/APP(安卓/iOS),重要的是对前端开发友好,自带的IDE让开发体验也挺棒的,公司项目就是主推uni-app

现公司今年准备新开一个项目,但是对项目的未来和项目要做的规格不是特别肯定,于是准备提前开发一个获客验证营销活动,试试市场反应

坑位

公司主要是做租赁业务的,公司为了降低产品的购买门槛,想通过依赖支付宝的芝麻免押来减免押金,但是芝麻免押并不是你想接就能直接接的,需要事先向支付宝申请获得相应能力后才可以接入,想申请芝麻免押能力,你得符合一定的条件产品介绍 - 支付宝文档中心

  • 申请要有完整的线上租赁流程,就是要已经是上线运行的小程序
  • 同时要符合租赁业务标准,在申请时候支付宝会回复不过的原因,按原因调整即可

活动很顺利的开发完成了,也在生产跑通了,现在就卡在申请芝麻免押的点上,产品为了申请芝麻免押一次次的调整需求,最后还打算专门做一套临时流程用于申请芝麻免押,临时流程开发进度到了80%的时候,突然申请又过了,最后就停掉了,最多一天是提了3版迭代,其中有一个0.0.4迭代已经过审(这里的过审是版本过审,不是芝麻免押过)到了可以发布的状态,但是产品觉得这个需求要调整下再重新提,先不要上,我于是完成调整需求,又重新提了迭代0.0.5,同时我又强迫症犯了,没忍住就把0.0.4迭代删了,当时再去提迭代0.0.6的时候,此时问题就产生了,外面显示的是0.0.6,但是里面还是0.0.4,,不管你怎么上传新迭代,它外面显示的是新版本号,但是里面还是那个0.0.4版本迭代,详见动画:

20250624_105830.gif

又因为线上版本高于0.0.4,导致迭代无法走下去,页面一直报当前版本低于线上版本的错误提示

Why?

已和支付宝技术人员做了多次沟通,暂时还没有定位到是什么问题,技术人员给的回复的,很大可能是开发平台那边的问题

image.png

如果后续有回复,我再更新文章

解决方案

方案1:

暂时不用要IDE的自动迭代了,先去后台手动新建一个,这样在上传的时候切换到新建的手动迭代就可以正常上传了

image 1.png

方案2:

把你已经新建的迭代,但是里面又锁定旧的迭代的那个迭代删除掉,再重新提迭代就行了,我目前就是这样解决的

删除迭代这里又有一个坑,删除迭代只能去迭代列表,在详情里是无法删的,但是迭代列表又看不到对应的版本号,分不清到底是哪一个迭代对应哪一个版本,只能通过发包时间去判断,这个问题我已经跟支付宝技术提了,他们说会反馈给管理平台优化

还有目前删除的迭代在详情页里还是会展示,但是点进去又会报错,这个也以反馈,说会优化

思考

遇到这种问题,直接找支付宝官方客服即可,描述好自己可复现路径或者说清楚出问题前做了哪些操作,再配合客服上传日志,如果比较急,可以讯问是否有临时方法先保证版本正常提,我当时就是急着发版,问的临时方法,也就是上面方案1,同时跟客服说一下,是不能请求加急下审核,以保证功能正常上线

开发者必看,全面解析 iOS 架构,探索 iOS 背后的秘密!

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

image.png

前言

苹果的移动设备,包括 iPhone、iPad 和 iPod,都是运行在 iOS 平台上,虽然后来 iPad 独立拆分出去成为了 iPadOS,但 iOS 和 iPadOS 的底层架构和 iOS 是相同的。

iOS 是基于 Darwin 为基础开发的。iOS 不仅可以管理硬件功能,还提供了开发 iOS 应用所需的一切技术支持。

iPhone 设备的基础软件包中包含一些常用的系统应用,例如邮件、日历、计算器、电话和浏览器等。然而,出于安全和商业原因,不可能在 Apple 以外的任何硬件上运行 iOS 和 Mac OS X,并且禁止在 Apple 以外的任何其他移动设备上使用 iOS。。

但是你有没有想过,iOS 的架构是怎样的?今天我们就来简单了解一下 iOS 的架构,如果未来你有机会研发一个新的操作系统,那么了解 iOS 的架构将对你有很大的帮助。

iOS 架构概述

iOS 架构的分层设计主要分为以下几层,可以从图中看得比较清楚:

image.png

  1. Core OS:核心操作系统层,负责管理硬件设备,提供内存管理、文件系统处理和线程等基本操作系统的功能。

  2. Core Services:核心服务层,将核心操作系统层提供的服务抽象化,提供所有应用可用的基本服务。

  3. Media:媒体层,通过 Media 层,开发者可以使用 iPhone 上的多媒体服务。

  4. Cocoa Touch:Cocoa Touch 层,提供了一个抽象层,使 iPhone 和其他 iOS 设备编程的各种库可访问。

iOS 会讲代码封装到框架(Framework)中。框架通常包括头文件、图片和所有所需的动态共享库。

多数 iOS 开发者在开始开发时倾向于选择更高级别的框架,因为它们提供了面向对象的抽象、更少的代码编写以及封装其他特性的能力。接下来让我们来探讨这些不同的抽象层次。

Core OS

作为 iOS 栈的最底层,Core OS 位于设备硬件之上。它提供了所有 iOS 功能的基础低级特性。除了基本操作系统的标准功能,如内存管理、文件系统处理和线程外,还提供了一些服务,包括低级网络和对外部配件的访问。

核心 OS 层储存了大多数其他技术的底层特性,比如。

  • Core Bluetooth Framework:与传统和低能耗蓝牙设备进行交互。

  • External Accessories Framework:与通过蓝牙或 Apple Lightning 连接的设备配件通信。

  • Accelerate Framework:进行大规模数学和图像计算,优化高性能和低能耗。

  • Security Services Framework:控制对应用及其维护数据的访问。

  • Local Authorization Framework:通过用户熟悉的生物识别或密码进行身份验证。

Core Services

Core Services 层将 Core OS 层提供的服务抽象化,提供所有应用可用的基本服务。核心服务层和其他层一样,提供了一组框架:

  • Accounts framework:允许用户直接从应用访问和管理其外部帐户,而无需输入登录信息。

  • Address Book framework:访问用户的联系信息。

  • CFNetwork framework:管理网络配置更改和网络服务访问。

  • Core Data framework:管理 MVC 应用的数据模型。

  • Core Foundation framework:提供基本数据管理和服务功能的接口。

  • Core Location framework:支持提供应用的位置信息和方向数据。

  • Core Media framework:提供音频和视频的播放、录制和处理。

  • Core Motion framework:提供设备的运动数据。

  • Core Telephony framework:提供电话网络服务。

  • EventKit framework:提供访问日历和提醒数据的功能。

  • Foundation framework:它为应用程序和框架提供了基础功能层,包括数据存储和持久化、文本处理、日期和时间计算、排序和过滤以及网络。对于 iOS 开发来说,Foundation 框架是使用最频繁的框架之一。

  • Mobile Core Services framework:该框架提供对重要操作系统功能的访问和控制。

  • Social framework:这个框架利用通用系统接口,将用户分享内容发布到支持的社交网站。

  • StoreKit:这个框架支持应用内购买以及与 App Store 的交互。

  • System Configuration framework:该框架允许应用程序访问设备上的网络配置选项,检查设备的可达性,例如 Wi-Fi 或蜂窝连接是否处于活动状态等。

Media Layer

通过 Media 层,开发者可以使用 iPhone 上的多媒体服务。主要包括系统的图形、音频和视频技术。苹果特别注重多媒体体验的反馈,尤其是音频和视频质量。所以 iOS 的底层媒体层赋予 iOS 音频、视频、图形和 AirPlay(无线传输)能力。

  • Assets Library framework:这个框架提供对用户资产的媒体库的访问。

  • AV Foundation framework:处理视听资源、管理相机设置、编辑音频并建立系统音频交互。

  • Core Audio framework:与设备的音频硬件接口。

  • Core Graphics framework:iOS 应用的本地绘图引擎,支持自定义 2D 矢量和基于图像的渲染。

  • Core Image framework:为控制视频和照片提供高级无损支持。

  • Core MIDI framework:提供与 MIDI(乐器数字接口)设备通信的 API,包括硬件键盘和合成器。

  • Core Text framework:提供了用于布局文本和处理字体的低级编程接口。

  • Core Video framework:利用基于管道的 API,支持 Metal 和 OpenGL 来处理数字视频,包括逐帧编辑。

  • Image I/O framework:这个框架提供对图像元数据的访问以及读取和写入大多数图像文件类型。

  • GLKit framework:使用硬件加速 API 管理高级 2D 和 3D 渲染。

  • Media Player framework:这个框架可以从应用程序中查找并播放歌曲、音频播客、有声读物和其他媒体。

  • OpenAL framework:这个框架提供了一种行业标准音频传输技术。

  • OpenGL ES framework:该框架通过硬件加速接口控制强大的 2D 和 3D 渲染。

  • Quartz Core framework:该框架允许用户使用幻灯片和 Core Image 滤镜查看、更改和保存照片。

Cocoa Touch

Cocoa Touch 层提供了一个抽象层,使 iPhone 和其他 iOS 设备编程的各种库可访问。Cocoa Touch 层包含了一组使用 Mac OS X Cocoa API 创建的 Objective-C 框架。

任何 iOS 应用的 UI 设计开发都是使用 Cocoa Touch 框架创建的。这一层支持通知、多任务处理、触摸特定输入、所有高级系统服务和其他重要技术。

  • Address Book UI framework:这个框架可以获取用户的联系人并将其呈现在图形界面中。

  • Event Kit UI framework:这个框架是一个用视图控制器来显示和更改事件的通用系统界面。

  • Game Kit framework:该框架允许用户通过游戏中心在线共享游戏相关数据。

  • iAd framework:广告框架。

  • Map Kit framework:地图框架。

  • Message UI framework:消息框架。

  • Twitter framework:用于创建推文以及创建 URL 来访问 Twitter 服务的用户界面。

  • WebKit framework:网页框架。

  • UI Kit framework:为 iOS 图形化、事件驱动的应用开发提供关键基础。

可以说做 iOS 开发,使用最多的就是 Foundation 和 UIKit 两个框架。

另外这一层还提供其他开发者使用的能力,例如:

  • 多任务处理。

  • 基本的应用程序管理和基础设施。

  • 用户界面管理。

  • 处理触摸事件。

  • 剪切、复制和粘贴等。

结论

综上所述,iOS 的架构设计合理,分层清晰,为开发者提供了丰富的框架和接口,使得开发 iOS 应用变得更加高效和便捷。

无论是从底层的 Core OS 到最上层的 Cocoa Touch,每一层都扮演着重要的角色,为用户提供了稳定、高效、安全的移动应用体验。深入了解 iOS 的架构,将有助于开发者更好地理解和利用 iOS 平台的强大功能,为开发出更优秀的应用奠定基础。

iOS 的成功不仅在于其设计和功能,更在于其庞大而完善的生态系统,相信随着技术的不断发展,iOS 将继续引领移动应用开发的潮流,为用户带来更多惊喜与便利。

你对 iOS 的架构有什么看法?欢迎在评论区留言讨论。

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

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

❌