阅读视图

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

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

JavaScriptCore 入门

背景

在现在大前端的概念越来越重要的背景下,在开发 iOS 应用时,我们常常需要在应用中执行 JavaScript 代码,或者在原生代码和 JS 之间进行交互。Apple 提供的 JavaScriptCore 框架,可以让我们在不依赖 WebView 的前提下,直接在 Objective-C 或 Swift 中嵌入 JavaScript 引擎,执行 JS 代码、传值、调用函数,从而实现双向通信。

在本篇文章中,会主要介绍 JavaScriptCore 的基本使用方式、核心类、双侧之间的互相调用以及异常处理。

首先,我们先来了解下什么是 JavaScriptCore。

JavaScriptCore 是什么?

JavaScriptCore 是 Apple 提供的一个框架,它封装了 WebKit 中的 JavaScript 引擎。通过它我们可以实现下面的功能:

  • 在 Native 应用中直接执行 JavaScript 代码;
  • 将 Native 对象或者方法暴露给 JavaScript 使用;
  • 调用 JS 函数并获取返回值;
  • 捕获处理 JS 代码中触发的异常;

了解完什么是 JavaScriptCore 以及它的使用场景,下面来看下它的核心类。

JavaScriptCore 核心类介绍

JavaScriptCore 中最常用的几个类包括以下四个:

  • JSContext:表示一个 JS 执行上下文环境(沙箱)
  • JSValue:JS 中的值在 Objective-C 中的包装
  • JSExport:通过协议导出原生方法和属性给 JS 使用
  • JSVirtualMachine:表示一个虚拟机,可用于多个 JSContext

通过这些类,我们就可以实现 Native 代码和 JS 代码之间的互相调用。

了解完概念,下面就开始写代码了。

Objective-C 调用 JavaScript 方法

示例代码如下:

JSContext *context = [[JSContext alloc] init];

[context evaluateScript:
 @"function add(x, y) { return x + y; }"];

JSValue *addFunc = context[@"add"];
JSValue *result = [addFunc callWithArguments:@[@5, @7]];

NSLog(@"add(5, 7) = %@", [result toNumber]); // 输出 12

首先,我们创建一个 JSContext 类型的实例对象 context 用来表示 JS 执行上下文。接着调用 evaluateScript 方法传递进去一个字符串,该字符串的内容是一个 JS 函数 add,用来计算两个参数之和。然后创建一个 JSValue 类型的对象用来接收 context 中的 add 方法。最后调用 callWithArguments 方法将需要计算的数字传递进去并将结果返回给 JSValue 类型的实例对象 result。

这就是在 Objective-C 调用 JavaScript 方法的流程。

打印结果如下:

add(5, 7) = 12

接着,我们再来看下如何在 JavaScript 调用 Objective-C 方法。

JavaScript 调用 Objective-C 方法

在 JavaScript 调用 Objective-C 方法要比在 Objective-C 调用 JavaScript 方法稍微复杂一点。需要下面三步:

  • 定义一个协议继承自 JSExport,将需要 JS 调用的方法放在协议里;
  • 声明类并实现这个协议;
  • 将类的实例对象注册给 JSContext;

示例代码如下:

// 第一步:声明协议
#import <JavaScriptCore/JavaScriptCore.h>

@protocol CalculatorExport <JSExport>

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2;

@end

// 第二步:实现协议
@interface Calculator : NSObject <CalculatorExport>
@end

@implementation Calculator

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2 {
    return num1 + num2;
}

@end

//将实例对象注册给 JSContext

JSContext *context = [[JSContext alloc] init];

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.addWithNum1Num2(3, 4);"];

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

结果:7

异常处理

在两侧联调开发时,不可避免的会出现代码方面的问题,这时候我们需要通过给 exceptionHandler 赋值,在回调中处理异常的场景。

示例代码如下:

JSContext *context = [[JSContext alloc] init];

context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
    NSLog(@"JS 异常:%@", exception);
};

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.sub(3, 4);"]; // 在 Native 侧,并没有导出 sub 方法

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

JS 异常:TypeError: calc.sub is not a function. (In 'calc.sub(3, 4)', 'calc.sub' is undefined)
结果:nan

如何提高前端应用的性能?

# 前端性能优化实战指南

## 1. 资源加载优化

### 1.1 代码拆分与懒加载
```javascript
// 动态导入实现懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

1.2 资源压缩与CDN

  • 使用Webpack的TerserPlugin压缩JS
  • 配置Gzip/Brotli压缩
  • 静态资源使用CDN加速

2. 渲染性能优化

2.1 虚拟列表

// 使用react-window实现虚拟滚动
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const Example = () => (
  <List
    height={600}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

2.2 避免强制同步布局

// 错误示例 - 导致布局抖动
function resizeAllParagraphs() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

// 正确示例 - 批量读取后再写入
function resizeAllParagraphs() {
  const width = box.offsetWidth;
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
  }
}

3. 内存管理

3.1 事件监听清理

useEffect(() => {
  const handleResize = () => {
    // 处理逻辑
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

3.2 图片优化

<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg"> 
  <img src="image.jpg" alt="描述文本">
</picture>

4. 缓存策略

4.1 Service Worker缓存

// 注册Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}

4.2 HTTP缓存头

Cache-Control: public, max-age=31536000
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

5. 监控与持续优化

5.1 性能指标监控

// 使用Performance API
const [entry] = performance.getEntriesByName('first-contentful-paint');
console.log('FCP:', entry.startTime);

5.2 Lighthouse自动化

// package.json
{
  "scripts": {
    "audit": "lighthouse http://example.com --output=json --output-path=./report.json"
  }
}

最佳实践总结

  1. 关键渲染路径优化

    • 内联关键CSS
    • 延迟非关键JS
    • 预加载重要资源
  2. 代码层面优化

    • 避免深层嵌套组件
    • 合理使用useMemo/useCallback
    • 减少不必要的重新渲染
  3. 网络层面优化

    • 使用HTTP/2
    • 实现资源预连接
    • 优化第三方脚本加载
  4. 持续监控

    • 建立性能基线
    • 设置性能预算
    • 自动化性能测试

通过以上方法的组合应用,可以显著提升前端应用的加载速度和运行时性能,提供更流畅的用户体验。

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的疑难杂症,助力每一位开发者!」

swift的get和set,newValue和oldValue

计算属性和存储属性都长什么样子,一定要记忆深刻

存储属性

var name: String
var name = "a"
var property: Int = {
return 1
}()

计算属性

class sample {
    var no1 = 0.0, no2 = 0.0
    var length = 300.0, breadth = 150.0

    var middle: (Double, Double) {
        get{
            return (length / 2, breadth / 2)
        }
        set(axis){ //注意这里的axis只是给newValue显示指定了参数名
            no1 = axis.0 - (length / 2)
            no2 = axis.1 - (breadth / 2)
        }
    }
}
或者
var computedValue: Int {
    get { _backingValue }
    set {
        // 使用隐式 newValue 在计算属性的setter中,如果不显式指定参数名,则默认使用`newValue`作为参数名。但是,计算属性没有内置的旧值(oldValue)访问,因为计算属性本身不存储值
        print("新值: \(newValue)")
        _backingValue = newValue
    }
}

在计算属性的setter中,如果不显式指定参数名,则默认使用`newValue`作为参数名。但是,计算属性没有内置的旧值(oldValue)访问,因为计算属性本身不存储值那么如果我们需要一个旧的值呢?需要手动存储旧值

private var _storage = 0
private var _oldValue = 0 // 额外存储旧值
var computedWithOldValue: Int {
    get { _storage }
    set {
        _oldValue = _storage  // 保存当前值为旧值
        _storage = newValue   // 更新为新值

        print("旧值: \(_oldValue), 新值: \(newValue)")
    }
}

只读的计算属性

var metaInfo: [String:String] {
        return [
            "head": self.head,
            "duration":"\(self.duration)"
        ]
    }
或者
var name: String {
       get {
           return ""
       }
   }

注意只读的计算属性并不是我们之前认识的readonly:因为只读计算属性在本类/结构体中也不能赋值图片那么如何实现readonly呢?

private(setvar name: String

这个也很好理解,是有set方法是private的,所以在类/结构体外还是可以get的

struct test1{
    private(set) var members :[String] = []
}

注意一个private(set)的集合,是不能添加和删除元素的

var t = test1()
t.members.append("a") //Cannot use mutating member on immutable value: 'members' setter is inaccessible

也可以结合计算属性使用

private var rawValue: Double = 0

private(set) var calibratedValue: Double {
    get { rawValue * 1.25 }
    set { rawValue = newValue / 1.25 }
}

关于计算属性的几个点

  • 存储属性我们可以定义常量或者是变量,但是对于计算属性,必须定义为变量,并且计算属性在定义时必须包含类型
  • 对于计算属性来说,set方法是可选的,而get方法必不可少
  • let的存储属性没有set方法,只读的计算属性area也没有set方法;所以我们不能简单的通过有没有set方法来区分属性是计算属性还是存储属性

接下来继续看一下协议

  • 协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。
  • 协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型
  • 在协议中,实例属性总是使用var声明为变量属性
  • 在协议中,始终使用static关键字作为类属性声明的前缀, 在类中实现时,可以使用classstatic关键字作类属性声明前缀(Class properties are only allowed within classes)
  • 协议还指定属性是可读的还是可读可写的

可读可写的属性在类型声明后通过写入{get set}表示可读属性通过写入{get}表示

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

如果协议要求属性是只读的,这意味着遵循协议的类型必须提供该属性,并且至少允许外部读取(get)。但是,在实现时,该属性可以是存储属性或计算属性,甚至可以是可读可写的(即可写的),因为协议只要求至少可读,不禁止可写。如下图,mustBeSettable至少要满足SomeProtocol中的“可读可写”图片属性观察者属性观察器(didSet 和 willSet)是在属性的值被修改时触发的。这种修改必须通过显式的赋值语法完成

self.property = newValue

这种是观察不到的:

@State private var blurAmount = 0.0 {
    didSet {
        print("New value is \(blurAmount)")
    }
}
Slider(value: $blurAmount, in: 0...20)
    .onChange(of: blurAmount) { newValue in
      print("New value is \(newValue)")
    }

除了在声明语句中对属性赋值,其他对属性做赋值操作,必会触发观察器属性观察器(Property Observers)提供了两个特殊的关键字 newValue 和oldValue,用于在属性值变化时访问新值和旧值。它们分别用于 willSet 和 didSet 观察器中

var property: DataType = initialValue {
    willSet {
        // 使用 newValue 访问即将设置的值
        // 当前属性值仍是旧值
    }
    didSet {
        // 使用 oldValue 访问被覆盖的值
        // 当前属性值已是新值
    }
}

willSet中自带一个newValue的属性,oldValue用property自身即可访问,相同的didSet自带一个oldValue的属性,newValue用property自身即可访问

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-)

UINavigationController 导航控制器

之前有一次设置了self.window.rootViewController = newViewController。但是没有把newViewController放入UINavigationController导航栈,结果一直跳转不了页面。浪费了很多时间。

UINavigationController导航控制器

UINavigationController,它用来管理视图控制器的层次结构,并提供导航栏(NavigationBar)来帮助用户在不同的视图控制器之间进行导航。通过这种方式,可以方便地实现页面之间的前进和后退操作。

使用UINavigationController

创建 UINavigationController

通常情况下,你会将一个视图控制器设置为根视图控制器(root view controller),然后把这个视图控制器传给 UINavigationController 的初始化方法。


       let viewController = ViewController()
        
        // 将它嵌入到导航控制器中
        let navController = UINavigationController(rootViewController: viewController)
        
        // 创建一个 UIWindow 实例,指定其 windowScene,并将导航控制器设置为其根视图控制器
        self.window = UIWindow(windowScene: windowScene)
        self.window?.rootViewController = navController
        self.window?.makeKeyAndVisible()

推送新视图控制器 页面导航

要从当前视图控制器导航到另一个视图控制器,你可以使用 pushViewController(_:animated:) 方法。

let nextViewController = NextViewController()
navigationController?.pushViewController(nextViewController, animated: true)

返回上一个视图控制器: 可以通过 popViewController(animated:) 方法返回到前一个视图控制器,或者使用 popToRootViewControllerAnimated(_:) 返回到根视图控制器。

Tab Bar Controller与Navigation Controller的2种组合方式

每个Tab包含一个Navigation Controller

这是最常见的方式,每个标签页都有自己的 UINavigationController 作为其根视图控制器。这样,每个标签都可以独立地处理自己的导航流程。

  • 创建多个 UINavigationController 实例,每个实例都包含一个不同的根视图控制器。
  • 将这些 UINavigationController 实例添加到 UITabBarController 的 viewControllers 属性中。
let tabBarController = UITabBarController()

let firstNavController = UINavigationController(rootViewController: FirstViewController())
let secondNavController = UINavigationController(rootViewController: SecondViewController())

tabBarController.viewControllers = [firstNavController, secondNavController]

单一Navigation Controller用于所有Tabs

这种方式不太常见,因为每个标签页共享同一个导航栈可能会导致用户体验不佳。不过,在某些特定场景下,这种模式也可能是合适的。

  • 在这种情况下,你首先需要创建一个 UINavigationController,然后将 UITabBarController 设置为其根视图控制器,或反之亦然,取决于具体需求。
        // 创建两个简单的View Controllers作为Tab的内容
        let vc1 = UIViewController()
        vc1.view.backgroundColor = .white
        vc1.tabBarItem.title = "首页"
        
        let vc2 = UIViewController()
        vc2.view.backgroundColor = .lightGray
        vc2.tabBarItem.title = "设置"
        
        // 创建Tab Bar Controller并添加View Controllers
        let tabBarController = UITabBarController()
        tabBarController.viewControllers = [vc1, vc2]
        
        // 创建Navigation Controller并将Tab Bar Controller设为其root view controller
        let navController = UINavigationController(rootViewController: tabBarController)
        
        // 设置window的rootViewController为Navigation Controller
        window?.rootViewController = navController
        window?.makeKeyAndVisible()

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新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

包瘦身之未引用图片资源扫描工具

未引用图片资源扫描工具

该工具用于扫描指定项目目录中的图片资源,并检测这些图片是否在代码中被引用,帮助开发者清理未使用的图片资源,节省项目体积和维护成本。


功能概述

  • 扫描图片资源
    支持扫描普通图片文件(.png.jpg.jpeg.gif.pdf)以及 .imageset 目录中的图片资源。
  • 扫描代码中的图片引用
    支持扫描 Objective-C 和 Swift 代码文件(.swift.m.mm.xib.storyboard),通过常见的图片引用方式(如 imageNamed:UIImage(named:)setImage:forState: 等)提取图片名。
  • 生成未引用图片列表
    将未被代码引用的图片路径写入输出文件 unused_images.txt,供开发者进一步确认和处理。

使用说明

1. 配置参数

  • PROJECT_PATH:项目路径,默认为当前目录下的 ./TestUnUserRes,请根据实际项目路径修改。
  • OUTPUT_FILE:输出文件名,默认为 unused_images.txt
  • IMAGE_EXTENSIONS:支持的图片文件后缀列表。
  • CODE_EXTENSIONS:支持扫描的代码文件后缀列表。

2. 扫描流程

  • 扫描图片资源
    遍历项目目录,收集所有符合后缀的图片文件和 .imageset 目录,图片名统一小写且不包含扩展名。
  • 扫描代码引用
    遍历代码文件,使用正则表达式匹配常见的图片引用方式,提取引用的图片名(去除路径和扩展名,统一小写)。
  • 对比并输出
    找出图片资源中未被代码引用的图片,将其完整路径写入输出文件。

3. 输出文件格式

  • 输出文件开头有提示文字:

    未被引用的图片:⚠️⚠️⚠️需要二次确认
    
  • 后续列出所有未被引用的 .png 格式图片的完整路径。


代码模块说明

find_images(project_path)

  • 输入:项目根路径
  • 输出:字典,键为图片名(不含扩展名,统一小写),值为对应图片文件或 .imageset 目录的完整路径列表
  • 功能:遍历目录收集所有图片资源

find_image_references(project_path)

  • 输入:项目根路径
  • 输出:集合,包含代码中引用的所有图片名(不含扩展名,统一小写)
  • 功能:遍历代码文件,使用正则表达式匹配图片引用

main()

  • 执行扫描流程,打印扫描结果,并将未引用图片写入输出文件。

注意事项

  • 仅支持部分常见图片引用方式,可能存在漏判或误判情况,输出结果需二次确认。
  • 只输出未引用的 .png 图片路径,其他格式未引用图片未写入文件。
  • 读取文件时默认使用 UTF-8 编码,若项目中有其他编码文件可能导致读取异常。
  • .imageset 目录视为一组图片资源,统一以目录名作为图片名处理。
  • 运行脚本前请确保 Python 环境已正确安装。

示例输出

未被引用的图片:⚠️⚠️⚠️需要二次确认
图片名称: ./TestUnUserRes/Assets.xcassets/Scaner/scaner_flashlight_on.imageset/scaner_flashlight_on@3x.png
图片名称: ./TestUnUserRes/Assets.xcassets/Scaner/nav_back_whiteArrow.imageset/nav_back_whiteArrow@3x.png
...

使用实例

见github上的demo

扩展建议

  • 增加对更多图片引用方式的支持,提高准确率。
  • 支持输出所有未引用图片格式,不限 .png
  • 支持多语言编码文件读取。
  • 增加命令行参数支持,方便自定义扫描路径和输出文件名。

如有疑问或需求,欢迎反馈和改进!# TestUnUserRes

Swift 协议之 Equatable

在 Swift 中,Equatable 是一个非常常见的协议。它的作用是判断两个值是否相等,是 Swift 中比较两个值最直接、最常见的方式。

如果我们需要判断两个字符串是否相等,通常会用下面的方式来实现:

let str1 = "Swift"
let str2 = "Swift"

let result = (str1 == str2)

那为什么字符串类型可以直接使用 == 操作符呢?答案就是因为系统已经给字符串类型实现了 Equatable 协议。同样的还有 IntArray 等系统类型都默认实现了该协议。

下面,我们先来看一下 Equatable 都包含什么内容。

Equatable 接口

Equatable 是一个标准库协议,它定义了一个基本的接口:

protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

从上面的源码可以看到,这个协议只要求实现一个方法:==

接下来,我们看一下什么场景下需要使用到这个协议。

Equatable 的使用场景

判断两个值是否相等,在日常开发中是非常常见的操作,比如以下的场景

  • 比较两个结构体是否代表同一个实体;
  • 判断数组中是否包含某个元素;
  • 在集合(如 Set)中去重;
  • 在 UI 中判断状态是否变化,是否需要刷新;

虽然,在 Swift 的标准库中,很多系统类型都遵守了 Equatable。但我们开发中不可能只使用标准库提供的类型,很多情况下,我们需要自定义类型。那么,如何让自定义的类型也能使用 == 操作符呢?

class Person {
    let name: String
    let id: Int
    init(name: String, id: Int) {
        self.name = name
        self.id = id
    }
}

let jack = Person(name: "jack", id: 123)
let rose = Person(name: "rose", id: 234)
print(jack == rose) // 编译报错 Binary operator '==' cannot be applied to two 'Person' operands

比如,上面我们自定义了一个 Person 的类,并且构建了两个实例对象,如果直接对两个对象使用 == 操作符,会导致上面的编译报错。

如何让自定义类型遵守 Equatable

对与我们自定义的类型,有两种方式可以遵守 Equatable。

方式一:自动合成

如果我们定义的结构体或枚举的所有成员都已经是遵守 Equatable 的类型,Swift 会自动帮你合成 == 实现。只要显式声明 Equatable,就能直接使用。比如我们上面举例的代码,我们只需要改动两个地方就可以让其遵守 Equatable 协议:

  • class 改为 struct;
  • 在 Person 后面显式的写出 Equatable 协议;

代码如下:

struct Person: Equatable {
    let name: String
    let id: Int
    init(name: String, id: Int) {
        self.name = name
        self.id = id
    }
}

let jack = Person(name: "jack", id: 123)
let rose = Person(name: "rose", id: 234)
print(jack == rose) 

需要注意的是:自动合成 == 只在以下条件下成立:

  • 所有属性都遵守 Equatable
  • 没有提供自定义的 == 实现;
  • 类型是结构体或者枚举;

如果我们想用类的话,或者想自定义比较逻辑的话。只能只用第二种方式:手动实现 Equatable 协议的方法。

方式二:手动实现

手动实现的示例代码如下:

extension Person: Equatable {
    static func == (lhs: Person, rhs: Person) -> Bool {
        lhs.id == rhs.id
    }
}

其余代码保持不变,我们只需用给 Person 添加一个扩展,并在扩展中实现 == 函数即可。这种方式更加灵活,因为我们可以在函数体里面自定义我们的比较逻辑。

Equatable 与泛型

当我们声明泛型函数的时候,可以给参数添加 Equatable 限制,以便进行参数比较,这样也可以更好的提高代码的健壮性。示例代码如下:

func areEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
    return a == b
}

print(areEqual(3, 3)) // true
print(areEqual("hi", "hello")) // false

let jack = Person(name: "jack", id: 123)
let rose = Person(name: "rose", id: 234)
print(areEqual(jack, rose)) // 如果 Person 没有遵守 Equatable协议的话,这一行会编译报错。

iOS26适配指南之Update Properties

介绍

  • UIViewController 与 UIView 均增加了一个名为updateProperties()的新方法,可以通过修改属性值达到更新 UI 的效果。
  • 它是一种轻量级的 UI 更新方式,不会触发完整的布局过程(不会触发layoutSubviews()或者viewWillLayoutSubviews()方法)。常见使用场景如下。
    • 更改 UI 的内容。
    • 显示/隐藏 UI。
    • 无需移动或者调整 UI 的大小。
  • 可以自动追踪 @Observable Object。
  • 可以通过调用setNeedsUpdateProperties()方法手动触发更新。

自动追踪

案例

import UIKit

@Observable class Model {
    var currentColor: UIColor = .systemGray
    var currentValue: String = "WWDC26"
}

class ViewController: UIViewController {
    lazy var label: UILabel = {
        let label = UILabel()
        label.frame = CGRect(x: 0, y: 0, width: 300, height: 60)
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 64)
        label.center = view.center
        return label
    }()
    let model = Model()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(label)
    }

    override func updateProperties() {
        super.updateProperties()

        label.textColor = model.currentColor
        label.text = model.currentValue
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        model.currentColor = .systemBlue
        model.currentValue = "iOS26"
    }
}

效果

自动追踪.gif

手动更新

案例

import UIKit

class Model {
    var currentColor: UIColor = .systemBlue
    var currentValue: String = "WWDC26"
}

class ViewController: UIViewController {
    lazy var label: UILabel = {
        let label = UILabel()
        label.frame = CGRect(x: 0, y: 0, width: 300, height: 60)
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 64)
        label.center = view.center
        return label
    }()
    let model = Model()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(label)
    }

    override func updateProperties() {
        super.updateProperties()

        label.textColor = model.currentColor
        label.text = model.currentValue
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        model.currentColor = .systemGray
        model.currentValue = "iOS26"
        // 手动更新
        setNeedsUpdateProperties()
    }
}

效果

手动更新

一行命令生成xcode自定义模板工程

工作中经常会创建一些Demo研究新特性、测试一些功能等,每次都要创建新工程写一些模板代码,很浪费时间,本文教你如何利用xcodegen生成自定义模板项目

xcodegen 简介

xcodegen是一个可以生成.xcodgeproj的命令行工具

xcodegen安装

利用Homebrew安装:

brew install xcodegen

yml文件

xcodegen根据yml配置生成.xcodeproj文件

name: MyApp  // 会生成名为:MyApp.xcodeproj的文件
options:
  bundleIdPrefix: com.example 
packages:   
  SnapKit:
    url: https://github.com/SnapKit/SnapKit.git
    from: 5.7.1
targets:
  MyApp:
    type: application
    platform: iOS
    sources:
      - path: Source  // 资源、源码等文件的路径
    info:
      path: Source/Info.plist
      properties:
        UILaunchStoryboardName: LaunchScreen
        UIMainStoryboardFile: Main
        UIApplicationSceneManifest:
          UIApplicationSupportsMultipleScenes: false
          UISceneConfigurations:
            UIWindowSceneSessionRoleApplication:
              - UISceneConfigurationName: "Default Configuration"
                UISceneDelegateClassName: "\$(PRODUCT_MODULE_NAME).SceneDelegate"
                UISceneStoryboardFile: "Main"

将上面配置保存为project.yml文件,把模板文件夹Source放到与yml同级目录里

文件夹名称Source就是yml配置的名称,如下图所示:

注意:xcodegen只能生成.xcodeproj文件,源码、资源等文件不会自动生成,只能通过yml中配置路径的方式引入

生成MyApp.xcodeproj文件

终端cd到project.yml所在的目录,然后执行

xcodegen generate

会生成一个名为MyApp.xcodeproj的文件,点击打开就可以运行了

一行命令生成xcode模板项目

通过上面的方式生成工程后,我们就可以制作这样的脚本了

创建脚本

#!/bin/bash

# 1. 获取参数
if [ -z "$1" ]; then
  echo "❌ 使用方法: 缺少参数 <ProjectName>"
  exit 1
fi

PROJECT_NAME=$1
SRC_DIR=$PROJECT_NAME

# 2. 检查 xcodegen
if ! command -v xcodegen &> /dev/null; then
    echo "❌ xcodegen 未安装,请执行:brew install xcodegen"
    exit 1
fi

# 3. 创建项目结构
mkdir -p "$PROJECT_NAME/$SRC_DIR"
cd "$PROJECT_NAME" || exit 1

# 4. 拷贝模板源码文件(假设放在脚本同级目录下的 TemplateFiles 目录中)
TEMPLATE_DIR="这里填写自定义模板文件路径"
if [ -d "$TEMPLATE_DIR" ]; then
  echo "📄 拷贝源码文件到 $SRC_DIR/ ..."
  cp -R "$TEMPLATE_DIR"/. "$SRC_DIR"/
else
  echo "⚠️ 未找到模板目录 $TEMPLATE_DIR,跳过拷贝"
fi

# 5. 创建 project.yml
cat > project.yml <<EOF
name: $PROJECT_NAME
options:
  bundleIdPrefix: com.example
packages:
  SnapKit:
    url: https://github.com/SnapKit/SnapKit.git
    from: 5.7.1
targets:
  $PROJECT_NAME:
    type: application
    platform: iOS
    sources:
      - path: $SRC_DIR
    info:
      path: $SRC_DIR/Info.plist
      properties:
        UILaunchStoryboardName: LaunchScreen
        UIMainStoryboardFile: Main
        UIApplicationSceneManifest:
          UIApplicationSupportsMultipleScenes: false
          UISceneConfigurations:
            UIWindowSceneSessionRoleApplication:
              - UISceneConfigurationName: "Default Configuration"
                UISceneDelegateClassName: "\$(PRODUCT_MODULE_NAME).SceneDelegate"
                UISceneStoryboardFile: "Main"
EOF

# 6. 生成并打开项目
echo "📦 生成 Xcode 工程..."
xcodegen generate

open "$PROJECT_NAME.xcodeproj"

将脚本保存为iosapp.sh,使用 chmod 命令给脚本文件添加执行权限:

chmod +x myscript.sh

执行脚本命令:

可以先这样执行脚本:

./iosapp.sh MyDemo

如果没问题,再将脚本移动到一个可以在终端中随时调用的命令:

sudo mv iosapp.sh /usr/local/bin/iosapp

然后直接使用下面方式执行脚本命令:

iosapp 项目名称

参考资料: github.com/yonaskolb/X…

日月之行,若出其中。星汉灿烂,若出其里。

three优化篇

1.模型简化

  • 型简化是通过减少多边形数(即顶点和面)来降低模型的复杂度。LOD(Level of Detail,细节层次)
  • Three.js支持LOD功能,能根据相机与模型的距离,自动切换不同细节的模型版本,从而减少渲染负荷。
import { LOD } from 'three';
 
const lod = new LOD();
 
// 设置低细节模型(适合远处显示)
const lowDetailMesh = createLowDetailMesh();
lod.addLevel(lowDetailMesh, 100); // 距离相机100单位时使用低细节模型
 
// 设置中细节模型
const mediumDetailMesh = createMediumDetailMesh();
lod.addLevel(mediumDetailMesh, 50); // 距离相机50单位时使用中细节模型
 
// 设置高细节模型(适合近距离显示)
const highDetailMesh = createHighDetailMesh();
lod.addLevel(highDetailMesh, 0); // 距离相机为0时使用高细节模型
 
scene.add(lod);

2.模型批处理

  • 批处理将多个小模型合并为一个大模型,减少WebGL的绘制调用次数,从而提高性能。在Three.js中,常用的批处理技术包括合并网格(Mesh)、实例化渲染(Instanced Rendering)等。
  • 可以将场景中多个静态对象合并成一个网格,从而减少渲染调用。
import { BufferGeometry, BoxGeometry, Mesh, MeshBasicMaterial, MeshStandardMaterial, Scene } from 'three';
 
// 创建材质和几何体
const material = new MeshStandardMaterial({ color: 0x00ff00 });
const geometry = new BoxGeometry();
 
// 创建多个网格
const meshes = [];
for (let i = 0; i < 10; i++) {
  const mesh = new Mesh(geometry, material);
  mesh.position.set(i * 2, 0, 0);
  meshes.push(mesh);
}
 
// 合并网格
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(meshes.map(m => m.geometry), true);
const mergedMesh = new Mesh(mergedGeometry, material);
scene.add(mergedMesh);

实例化渲染:使用InstancedMesh可以实现一次性渲染多个相同的网格,提高渲染效率。

import { InstancedMesh, BoxGeometry, MeshBasicMaterial, Matrix4 } from 'three';
 
const geometry = new BoxGeometry();
const material = new MeshBasicMaterial({ color: 0x00ff00 });
const count = 100;
 
const instancedMesh = new InstancedMesh(geometry, material, count);
 
for (let i = 0; i < count; i++) {
  const matrix = new Matrix4();
  matrix.setPosition(i % 10, Math.floor(i / 10), 0);  // 设置位置
  instancedMesh.setMatrixAt(i, matrix);
}
 
scene.add(instancedMesh);

硬件加速与性能优化技巧

硬件加速和性能优化是确保Three.js应用在多种设备上流畅运行的关键。优化渲染管道和减少GPU负荷能有效提高应用的响应速度。

  • 使用帧率限制和动态分辨率。通过调整渲染器的更新时间,控制最大帧率。
let lastRenderTime = 0;
const maxFPS = 30;
 
function animate(time) {
  const delta = time - lastRenderTime;
  if (delta > 1000 / maxFPS) {
    renderer.render(scene, camera);
    lastRenderTime = time;
  }
  requestAnimationFrame(animate);
}
animate(0);

动态分辨率:动态调整渲染分辨率,在保持画质的同时降低渲染负担。Three.js中可以通过调整渲染器的setPixelRatio来实现。

renderer.setPixelRatio(window.devicePixelRatio > 1 ? 1.5 : 1);

场景剔除和遮挡剔除

  • 场景剔除:Three.js中的场景剔除技术会自动隐藏相机视野外的对象。可以进一步使用分区剔除技术来优化大型场景。
  • 遮挡剔除:在一些场景中,对被完全遮挡的物体进行剔除,可以减轻渲染负担。

清除不必要场景模型

/* 清除不再使用内存模型几何体与材质 防止泄露 */
mesh.remove()
1.
mesh.traverse((obj:any)=> {
    if (obj.type === 'Mesh') {
      obj.geometry.dispose();
      obj.material.dispose();
    }
})

2.
model.traverse((obj) => {
    if (!obj.isMesh) return;
    obj.geometry.dispose();
    obj.material.dispose();
});
model = null;

节流渲染 requestAnimationFrame()

-面板超出一定时间停止渲染、鼠标事件触发重新渲染

1. 节流渲染
let timeOut;
let render = () => {
    stats.update();
    if (timeOut) {
      controls.update();
      composer.render();
    }
    /* 动画事件 */
    requestAnimationFrame(render);
};render()


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-)

❌