UIKit/CA

312 阅读54分钟

参考:

juejin.cn/post/684490…

juejin.cn/post/735538…

1、UIkit

1.1 UITableViewUICollectionView、UIScrollView

UITableViewUICollectionView是iOS开发中常用的两个用于展示列表数据的控件。它们都可以用来展示数据集合,但是在布局、灵活性和使用场景上有所不同。

UITableView

UITableView主要用于展示单列的列表数据,它的每一行(cell)可以展示相同或不同类型的数据。UITableView非常适合用于展示简单的垂直滚动列表

特性

  • 单列布局,每个section中的cell垂直排列。

  • 支持自定义cell。

  • 支持section header和footer。

  • 支持行的插入、删除和重新排序操作。

  • 内置了列表项的选择和编辑模式。

UICollectionView

**UICollectionView**提供了更为灵活的方式来展示数据集合,不仅支持单列布局,还支持多列布局、网格布局以及更复杂的自定义布局

特性

  • 支持多列布局和自定义布局,如网格布局、瀑布流等。

  • 支持自定义cell以及补充视图(如header和footer)。

  • 支持section,每个section可以有自己的布局。

  • 支持行的插入、删除和重新排序操作。

  • 灵活的布局系统,可以通过自定义UICollectionViewLayout来实现复杂的布局需求。

UITableView与UICollectionView的区别

  • 布局灵活性UICollectionView在布局上比UITableView更加灵活, UITableView仅支持单列垂直滚动的布局,而UICollectionView支持多种布局,包括网格布局、瀑布流布局等。

  • 使用场景UITableView适用于简单列表的展示,如设置页面、新闻列表等;UICollectionView适用于需要更复杂布局的场景,如图片库、商品展示等。

  • 布局自定义UICollectionView可以通过自定义UICollectionViewLayout来实现复杂的布局需求,而UITableView的自定义程度相对较低。

  • 性能:在展示大量数据项时,两者都进行了优化以提高性能,如重用机制。但由于UICollectionView的布局更加复杂,所以在处理大量数据和复杂布局时可能会稍微影响性能。

总的来说,UITableView适合用于展示结构简单的列表数据,而UICollectionView提供了更高的灵活性和自定义能力,适合用于需要复杂布局的场景。开发者可以根据具体的需求选择使用哪一个控件。

UItableView如何控制单条cell的刷新

在 iOS 开发中,UITableView 提供了灵活的方法来控制单条或多条 UITableViewCell 的刷新。当你想要更新表格中某个特定单元格的内容时,可以使用以下方法之一来实现单条 cell 的刷新,而不需要重新加载整个表格视图。

使用 -reloadRowsAtIndexPaths:withRowAnimation: 方法

这是刷新单条或多条 cell 最常用的方法。你需要提供一个包含一个或多个 NSIndexPath 对象的数组,这些 NSIndexPath 对象指定了表格视图中你想要刷新的行。同时,你可以指定一个动画效果,以控制 cell 刷新时的动画。

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowNumber inSection:sectionNumber];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];

// Swift 示例
let indexPath = IndexPath(row: rowNumber, section: sectionNumber)
tableView.reloadRows(at: [indexPath], with: .fade)

### 使用 `-beginUpdates` 和 `-endUpdates` 方法
如果你的 `cell` 刷新操作需要伴随行高的变化,可以在 `-beginUpdates` 和 `-endUpdates` 方法之间执行 `-reloadRowsAtIndexPaths:withRowAnimation:` 方法。
这样做可以让 `UITableView` 在刷新 `cell` 的同时,对行高进行适当的调整。

// Objective-C 示例
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];


// Swift 示例
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .fade)
tableView.endUpdates()

注意事项

  • 在调用 **-reloadRowsAtIndexPaths:withRowAnimation: 方法之前,确保你已经更新了数据源中对应 cell 的数据。**

这是因为 UITableView 会根据当前的数据源来重新配置和显示 cell

  • 如果你只是简单地更新 cell 的视觉样式,而不是内容,有时直接获取 cell 对象并修改它可能更高效。

但是,这种方法不会触发 cell 的重新配置,所以如果数据发生了变化,你应该使用上述的刷新方法。

通过以上方法,你可以灵活地控制 UITableView 中单条 cell 的刷新,从而提高应用的性能和用户体验。

UITableView嵌套横向滑动的UIScrollView

UITableView嵌套横向滑动的UIScrollView的场景中,如果在UIScrollView内部进行上下滑动操作,理论上是不会影响外层UITableView的滚动行为的,因为UIScrollView的滚动方向是横向的,UITableView的滚动方向是纵向的。系统能够根据滑动方向判断哪个滚动视图应该接收滑动事件。

然而,在实际开发中,可能会遇到一些特殊情况,导致用户在UIScrollView内部进行上下滑动时,外层UITableView也响应了滚动事件

。这种情况通常是由于滚动事件的传递和处理机制导致的。为了确保用户体验,可以采取以下措施来优化这种嵌套滚动的场景:

1. 禁用UIScrollView的垂直滚动

由于UIScrollView是横向滚动的,可以通过禁用其垂直滚动来避免它接收垂直方向的滚动事件:


scrollView.showsVerticalScrollIndicator = NO;

scrollView.alwaysBounceVertical = NO;

这样,当用户尝试在UIScrollView内进行上下滑动时,UIScrollView不会响应这个滚动事件,从而避免影响到外层的UITableView

2. 自定义UIScrollView的触摸事件处理

如果需要更精细的控制滚动事件的处理,可以通过子类化UIScrollView并重写触摸事件相关的方法(如touchesBegan:withEvent:touchesMoved:withEvent:等)来自定义事件的处理逻辑。例如,可以在这些方法中判断滑动的方向,只有当检测到是横向滑动时,才让UIScrollView处理滚动事件。

3. 使用UIScrollView的代理方法控制滚动行为

UIScrollView的代理(UIScrollViewDelegate)提供了一系列方法来监控和控制滚动行为。可以通过实现这些代理方法来精确控制何时允许滚动,例如:


- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

if (scrollView == self.myNestedScrollView) {

// 检查scrollView的滚动方向,根据需要启用或禁用外层UITableView的滚动

}

}

总结

UITableView嵌套横向滑动的UIScrollView时,通常情况下,UIScrollView内部的上下滑动操作不会影响到外层UITableView的滚动行为。但是,如果遇到特殊情况需要控制滚动事件的传递,可以通过上述方法来优化用户体验。关键是根据实际的应用场景和用户需求,选择合适的方法来实现滚动行为的精确控制。

Uitableview或者UIScrollView滑动后如何拿到当前展示的cell

点击展开内容

在iOS开发中,有时候我们需要知道UITableViewUIScrollView滑动后,哪些cell或视图当前正在显示。这可以通过各自的API来实现。

UITableView

对于UITableView,可以使用visibleCells属性或indexPathsForVisibleRows方法来获取当前显示的cell或它们的索引路径。

获取当前显示的cell


NSArray<UITableViewCell *> *visibleCells = [self.tableView visibleCells];

获取当前显示的cell的索引路径


NSArray<NSIndexPath *> *visibleIndexPaths = [self.tableView indexPathsForVisibleRows];

UIScrollView

对于UIScrollView(以及它的子类,如UICollectionView),因为它不像UITableView那样有直接获取可见cell的方法,所以需要通过计算来实现。基本思路是遍历UIScrollViewsubviews,检查哪些视图是在当前的可视区域内。

示例代码


// 假设scrollView是你的UIScrollView实例

for (UIView *subview in scrollView.subviews) {

// 判断子视图是否在当前可视区域内

if ([scrollView boundsIntersectsRect:subview.frame]) {

// 这里处理你对可见视图的操作

}

}

// 辅助方法,用于判断视图是否在UIScrollView的可视区域内

- (BOOL)boundsIntersectsRect:(CGRect)rect inScrollView:(UIScrollView *)scrollView {

CGRect visibleRect;

visibleRect.origin = scrollView.contentOffset;

visibleRect.size = scrollView.bounds.size;

return CGRectIntersectsRect(rect, visibleRect);

}

UICollectionView

对于UICollectionView,可以使用类似UITableView的方法来获取当前可见的cell或它们的索引路径。

获取当前显示的cell


NSArray<UICollectionViewCell *> *visibleCells = [self.collectionView visibleCells];

获取当前显示的cell的索引路径


NSArray<NSIndexPath *> *visibleIndexPaths = [self.collectionView indexPathsForVisibleItems];

总结

  • 对于UITableViewUICollectionView,可以直接使用提供的API来获取当前显示的cell或它们的索引路径。

  • 对于UIScrollView,需要通过遍历其subviews并计算它们是否在当前的可视区域内来判断哪些视图是可见的。

contentInset和contentoffset

在iOS开发中,UIScrollView及其子类(如UITableViewUICollectionView)用于展示可以滚动的内容。

contentInsetcontentOffsetUIScrollView中两个重要的属性,它们对于控制滚动视图的布局和行为至关重要。

contentInset

contentInset属性允许你增加额外的滚动区域到UIScrollView的四周。这是一个UIEdgeInsets类型的值,包含四个值:topleftbottomright。通过设置这些值,你可以在滚动视图的内容周围增加额外的空间,这对于避免内容被导航栏、标签栏或其他界面元素遮挡非常有用

例如,如果你有一个全屏的UITableView,并且不希望顶部的内容被导航栏遮挡,你可以设置contentInsettop值为导航栏的高度,这样顶部的内容就会下移,不被导航栏遮挡。


tableView.contentInset = UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0)

contentOffset

contentOffset属性表示滚动视图内容当前滚动的位置。这是一个CGPoint值,指定了内容视图左上角相对于滚动视图原点的偏移量。通过修改contentOffset,你可以控制滚动视图的显示区域。

例如,如果你想要滚动到UIScrollView的底部,你可以设置contentOffset.y为滚动视图内容高度减去视图高度。


let contentHeight = scrollView.contentSize.height

let scrollViewHeight = scrollView.bounds.size.height

scrollView.contentOffset = CGPoint(x: 0, y: contentHeight - scrollViewHeight)

总结

  • contentInset允许在滚动视图的四周增加额外的滚动区域,常用于避免内容被其他UI元素遮挡。

  • contentOffset表示滚动视图当前的滚动位置,可以用来控制滚动视图的显示区域。

  • 两者结合使用可以提供强大的控制能力,以实现复杂的滚动效果和布局需求。

1.2 UIResponderUIControl、UITouch

UIResponderUIControl是UIKit框架中用于处理用户交互的两个重要类,它们在iOS应用的事件处理中扮演着关键角色。尽管它们都与事件处理相关,但它们的用途和工作方式有所不同。

UIResponder

UIResponder是一个抽象类,定义了响应和处理事件的接口。它是所有能够响应事件的对象的基类,包括UIViewUIViewControllerUIApplication等。

UIResponder创建了一个事件响应链,该链用于将事件(如触摸、运动事件和远程控制事件)从一个响应者传递到下一个响应者。事件响应链允许事件在视图层次结构中逐级向上传递,直到找到一个能够处理该事件的对象。

UIResponder的主要职责包括:

  • 处理触摸事件:通过touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:touchesCancelled:withEvent:方法。

  • 处理运动事件:如摇晃设备。

  • 处理远程控制事件:如耳机控制。

  • 成为第一响应者:能够接收到非触摸类事件,如键盘输入。

UIControl

UIControlUIView的一个子类,专门用于处理用户交互的控件,如按钮(UIButton)、开关(UISwitch)、滑动条(UISlider)等。

UIControl提供了一种机制,允许控件在特定事件发生时向一个或多个目标对象发送消息。这是通过target-action机制实现的。

UIControl的主要特点包括:

  • 用户交互UIControl对象是专门设计来响应用户交互的,如点击、拖动等。

  • Target-Action机制:可以为UIControl对象的事件(如点击)设置一个或多个目标(target)和动作(action)。当事件发生时,控件会向目标发送动作消息。

  • 事件处理UIControl定义了一系列控件特有的事件,如UIControlEventTouchUpInside(按钮点击)等,并允许为这些事件添加处理器。

总结

  • UIResponder是一个抽象类,定义了事件响应的基本接口,是所有能够响应事件的UI组件的基类。它负责处理触摸事件、运动事件等,并通过事件响应链传递事件。

  • UIControlUIResponder的子类,专门用于创建交互式的UI控件,如按钮和滑动条。它通过target-action机制允许控件在用户交互时执行特定的动作。

在iOS应用开发中,理解UIResponderUIControl的区别和联系对于正确处理用户交互和事件传递至关重要。

UITouch

UITouch是iOS开发中UIKit框架提供的一个类,用于表示在屏幕上发生的一个触摸事件。每个UITouch对象包含了触摸事件的信息,如触摸的位置、触摸的阶段(如开始触摸、移动、结束等),以及触摸发生时的时间戳等。通过这些信息,开发者可以了解用户与应用界面交互的细节,并据此实现相应的响应逻辑。

### UITouch的主要属性和方法
- **locationInView:** 方法可以获取触摸在指定视图坐标系统中的位置这是处理触摸事件时最常用的信息之一
  CGPoint touchPoint = [touch locationInView:self.view];
- **previousLocationInView:** 方法可以获取触摸在指定视图坐标系统中的前一个位置这对于计算触摸移动的方向和距离非常有用
  CGPoint previousTouchPoint = [touch previousLocationInView:self.view];
- **phase:** 属性表示触摸的当前阶段,它的类型是`UITouchPhase`枚举,包括`UITouchPhaseBegan``UITouchPhaseMoved``UITouchPhaseStationary``UITouchPhaseEnded`和`UITouchPhaseCancelled`等值,分别表示触摸开始移动静止结束和取消
- **timestamp:** 属性表示触摸事件发生的时间戳这可以用来计算触摸事件的持续时间或者两次触摸事件之间的时间间隔
- **view:** 属性表示触摸发生在哪个视图上这可以帮助开发者确定触摸事件的上下文
- **window:** 属性表示触摸发生在哪个窗口上

### 处理UITouch事件
在iOS应用中,`UIView`和`UIViewController`的子类可以通过重写`touchesBegan:withEvent:``touchesMoved:withEvent:``touchesEnded:withEvent:`和`touchesCancelled:withEvent:`方法来处理触摸事件
这些方法由`UIResponder`类提供,是处理触摸事件的基础
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self.view];
    // 处理触摸开始的逻辑
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    UITouch *touch = [touches anyObject];

    CGPoint touchPoint = [touch locationInView:self.view];

    // 处理触摸移动的逻辑

}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 处理触摸结束的逻辑

}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 处理触摸取消的逻辑

}

通过这些方法,开发者可以实现各种触摸交互效果,如拖拽滑动点击等`UITouch`类提供的信息使得开发者能够精确地控制应用对用户触摸的响应

2、Core Animation是一个负责处理图层、屏幕绘制、动画等的复合引擎--Layer Kit

职责就是尽可能快地组合屏幕上不同的可视内容。这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于 是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。

CALayer 是更底层的东西,UIView 其实就是对 CALayer 的高级封装每个UIView都包含一个CALayer实例,也就是所谓的backing layer

CALayer 与 UIView 最大的区别是它不处理用户的交互,它的职责是负责屏幕上的显示和动画。UIView负责处理用户交互,并管理CALayer。

和 UIView 最大的不同是 CALayer 不 处理用户的交互。 CALayer 并不清楚具体的响应链(iOS通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断是否一个触点在图层的范围之内。

为什么要基于 UIView 和 CALayer 提供两个平行的层级关系呢?为什 么不用一个简单的层级来处理所有事情呢?

答:原因在于要做职责分离,这样也能避免很多重复代码。在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同, 基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有 UIKit和 UIView ,但是Mac OS有AppKit和 NSView 的原因。他们功能上很相似, 但是在实现上有着显著的区别。

还有一个很重要的点,直接使用 CALayer 不能用自动布局,需要自己计算布局。所以一般我们不会直接使用 CALayer。除非一些比较特殊的需求。

同时,已经证实了图层不能像视图那样处理触摸事件,那么它能做哪些视图不能做的呢?这里有一些 UIView 没有暴露出来的CALayer的功能:

  • 阴影,圆角,带颜色的边框 
  • 3D变换 
  • 非矩形范围 
  • 透明遮罩 
  • 多级非线性动画

Core Animation 提供了多种类型的动画,包括基础动画(CABasicAnimation)、关键帧动画(CAKeyframeAnimation)和过渡动画(CATransition)

  1. 设置 CALayer 的 backing image 有两种方式:

    • 直接设置 contents 属性
    • 实现 drawRect 方法,用 Core Graphics 绘制

2.1 隐式动画/显式动画

在Core Animation(CA)框架中,隐式动画(Implicit Animations)和显式动画(Explicit Animations)是两种常见的动画实现方式,它们在使用方法和控制粒度上有所不同。

隐式动画

隐式动画是指当动画对象的属性发生变化时,动画效果自动应用的动画。在Core Animation中,大多数CALayer的属性变化都会自动产生动画效果,这种动画就是隐式动画。隐式动画的特点是简单易用,不需要编写专门的动画代码,仅通过改变属性值即可实现动画效果。

  • 优点:简单易用,代码量少。

  • 缺点动画效果和参数较为固定,自定义程度低。

// 示例:改变图层的背景色
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:layer];

// 改变图层的背景色,将自动产生渐变色变化的动画效果
[CATransaction begin];
[CATransaction setAnimationDuration:1.0]; // 设置动画持续时间
layer.backgroundColor = [UIColor redColor].CGColor;
[CATransaction commit];

显式动画

显式动画是指开发者通过创建动画对象(如CABasicAnimation、CAKeyframeAnimation等),并将其添加到图层上来显式地控制动画效果的动画。显式动画提供了更多的自定义选项,包括动画类型、持续时间、重复次数、动画曲线等。

  • 优点高度可定制,可以实现复杂和精细的动画效果。

  • 缺点:需要编写更多的代码,使用起来相对复杂。

// 示例:创建一个旋转动画
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0]; // 旋转360度
rotationAnimation.duration = 1.0; // 动画持续时间
rotationAnimation.repeatCount = HUGE_VALF; // 重复次数,这里设置为无限重复

// 将动画添加到图层上
[layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];

区别

  1. 实现方式隐式动画通过改变CALayer的属性来自动触发,而显式动画需要创建动画对象并添加到图层上

  2. 控制粒度:显式动画提供了更多的自定义选项,可以精细控制动画的各个方面;隐式动画的自定义程度较低。

  3. 使用场景:对于简单的动画效果,使用隐式动画更为方便快捷;而对于需要精细控制或实现复杂动画效果的场景,显式动画是更好的选择。

禁用隐式动画

有时候,我们可能不希望CALayer的属性变化时自动产生动画效果。在这种情况下,可以通过事务(CATransaction)来禁用隐式动画:

[CATransaction begin];

[CATransaction setDisableActions:YES];

// 改变layer的属性,此时不会有隐式动画
layer.opacity = 0.5;

[CATransaction commit];

通过上述方法,可以根据需要选择使用隐式动画还是显式动画,或者在特定情况下禁用隐式动画。

2.2 贝塞尔曲线

在 Objective-C 中,贝塞尔曲线(Bezier Curve)是一种在图形软件中广泛使用的数学曲线,用于模型平滑曲线,它可以通过简单的数学公式定义,便于计算机图形系统渲染。UIKit 提供了 UIBezierPath 类来绘制贝塞尔曲线,它支持创建基本的图形(如矩形、圆形)到复杂的自定义路径。

点击展开内容

UIBezierPath 基本使用

  1. 创建 UIBezierPath 对象

UIBezierPath *path = [[UIBezierPath alloc] init];

  1. 移动到起点

[path moveToPoint:CGPointMake(100, 100)];

  1. 添加线段

[path addLineToPoint:CGPointMake(200, 100)];

[path addLineToPoint:CGPointMake(200, 200)];

  1. 关闭路径

如果你想要闭合路径(即连接路径的终点和起点),可以使用:


[path closePath];

  1. 绘制曲线

贝塞尔曲线的绘制通常在 UIView 的 -drawRect: 方法中进行:


- (void)drawRect:(CGRect)rect {

// 设置曲线颜色

[[UIColor blueColor] setStroke];

// 绘制路径

[path stroke];

}

绘制贝塞尔曲线

贝塞尔曲线可以是简单的直线,也可以是复杂的曲线路径。例如,绘制一个二次贝塞尔曲线:


UIBezierPath *quadPath = [[UIBezierPath alloc] init];

[quadPath moveToPoint:CGPointMake(10, 150)]; // 起点

[quadPath addQuadCurveToPoint:CGPointMake(300, 150) controlPoint:CGPointMake(150, 0)]; // 终点和控制点

[[UIColor redColor] setFill];

[quadPath stroke];

绘制一个三次贝塞尔曲线:


UIBezierPath *cubicPath = [[UIBezierPath alloc] init];

[cubicPath moveToPoint:CGPointMake(10, 150)]; // 起点

[cubicPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(100, 0) controlPoint2:CGPointMake(200, 300)]; // 终点和两个控制点

[[UIColor greenColor] setFill];

[cubicPath stroke];

注意事项

  • 在使用 UIBezierPath 绘制路径时,需要在 UIView 的 -drawRect: 方法中进行,因为这个方法提供了绘图上下文(Graphics Context)。

  • 使用 [path stroke] 来绘制路径的轮廓,使用 [path fill] 来填充路径内部。

  • 控制点的位置决定了贝塞尔曲线的弯曲程度和方向。在绘制复杂图形时,可能需要通过调整控制点来获得期望的曲线形状。

2.3 关键帧动画

关键帧动画(Keyframe Animation)是一种在动画开始和结束之间,通过定义一系列关键帧来控制动画状态变化的技术。在 iOS 开发中,可以使用 Core Animation 框架来实现关键帧动画,主要通过 CAKeyframeAnimation 类来完成。

点击展开内容

基本使用

  1. 创建 CAKeyframeAnimation 对象

首先,创建一个 CAKeyframeAnimation 对象,并指定要动画的属性名称,例如 positionopacity 等。


CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];

  1. 设置关键帧

你可以通过设置 values 属性来定义动画的关键帧。这是一个数组,包含了动画过程中每个关键帧的属性值。


animation.values = @[

[NSValue valueWithCGPoint:CGPointMake(100, 100)],

[NSValue valueWithCGPoint:CGPointMake(200, 100)],

[NSValue valueWithCGPoint:CGPointMake(200, 200)],

[NSValue valueWithCGPoint:CGPointMake(100, 200)],

[NSValue valueWithCGPoint:CGPointMake(100, 100)]

];

或者,使用 path 属性来指定一个路径,动画将沿着这个路径进行。


CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, 100, 100);

CGPathAddLineToPoint(path, NULL, 200, 100);

CGPathAddLineToPoint(path, NULL, 200, 200);

CGPathAddLineToPoint(path, NULL, 100, 200);

CGPathCloseSubpath(path);

animation.path = path;

CGPathRelease(path);

  1. 设置其他动画属性

除了关键帧之外,还可以设置动画的其他属性,如动画时长、重复次数等。


animation.duration = 4.0; // 动画时长

animation.repeatCount = HUGE_VALF; // 重复次数,HUGE_VALF 表示无限重复

  1. 将动画添加到图层

最后,将 CAKeyframeAnimation 对象添加到图层(CALayer)上,开始执行动画。


[someLayer addAnimation:animation forKey:@"positionAnimation"];

注意事项

  • 关键帧动画可以应用于图层的任何可动画属性。

  • 使用 values 属性时,你需要手动指定动画的每个关键帧值;使用 path 属性时,动画将沿着路径自动生成关键帧。

  • 动画完成后,默认情况下动画对象会从图层上移除,并且图层会回到动画开始之前的状态。如果你想让图层保持在动画结束的状态,需要设置动画对象的 fillModeremovedOnCompletion 属性。

  • 关键帧动画只影响图层的表现,并不改变图层的实际属性值。

通过使用关键帧动画,你可以创建复杂的动画效果,如弹跳、摇摆等,为你的应用增添更多动态和趣味性。

2.4 CADisplayLink

CADisplayLink是iOS开发中用于同步屏幕刷新率进行定时操作的一个工具,它属于Core Animation框架

通过使用CADisplayLink,你可以精确地控制动画或其他视觉更新的时机,以确保动画的流畅性。

CADisplayLink会在每次屏幕刷新时被触发,使得你的动画或绘图操作能够与设备的屏幕刷新率同步。

### 创建和使用`CADisplayLink`
1. **创建`CADisplayLink`实例**:通过`CADisplayLink`的`+displayLinkWithTarget:selector:`方法创建实例,并指定一个目标对象和一个在屏幕每次刷新时调用的选择器(方法)。
2. **将`CADisplayLink`添加到运行循环**:创建`CADisplayLink`后,需要将其添加到一个运行循环中,并指定运行循环模式。通常,它被添加到主运行循环中,使用默认的运行循环模式。
3. **开始和停止`CADisplayLink`**:可以通过设置`CADisplayLink`的`paused`属性为`YES`或`NO`来停止或开始定时器。
4. **移除`CADisplayLink`**:当不再需要`CADisplayLink`时,应该将其从运行循环中移除,并将其置为`nil`,以避免内存泄漏。

### 示例代码
#import <QuartzCore/QuartzCore.h>

@interface MyViewController ()
@property (strong, nonatomic) CADisplayLink *displayLink;
@end

@implementation MyViewController

- (void)viewDidLoad {

    [super viewDidLoad];
    // 创建CADisplayLink,并将其添加到主运行循环
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

}

- (void)handleDisplayLink:(CADisplayLink *)displayLink {
    // 这个方法会在屏幕每次刷新时调用
    // 在这里执行你的动画更新或其他定时操作
}

- (void)dealloc {
    // 移除CADisplayLink以避免内存泄漏
    [self.displayLink invalidate];
    self.displayLink = nil;
}

@end


### 注意事项
- **性能**:由于`CADisplayLink`会在每次屏幕刷新时触发,如果你的处理逻辑过于复杂,可能会影响应用的性能和动画的流畅性。因此,确保你的处理逻辑尽可能高效。
- **内存管理**:为了避免循环引用,当目标对象是`self`时,应该在适当的时候(如视图控制器被销毁时)停止并移除`CADisplayLink`。
- **帧率调整**:可以通过`CADisplayLink`的`frameInterval`属性(在iOS10及以上,使用`preferredFramesPerSecond`)来调整触发频率,以减少资源消耗。
`CADisplayLink`是实现高性能动画和视觉效果的强大工具,通过与设备的屏幕刷新率同步,可以创建出非常平滑和自然的动画效果。

3、UIKit与SwiftUI

juejin.cn/post/712879…

juejin.cn/post/732772…

3.1 UIKit

UIKit是一个框架,使你能够构建用户界面(UI),可以处理触摸事件和输入,同时管理用户、系统和你的应用程序之间的互动。

UIKit是CocoaTouch的一部分,它在2008年作为iOS SDK的一部分发布,并在iOS的第一个公开版本(当时被称为iPhoneOS)中提供。

在此之前,UIKit是为iOS的前两个版本私人开发的。UIKit于2008年公开发布,而且没有任何被苹果抛弃的迹象。有了UIKit,在构建iOS应用时,你可以获得最完整、功能最齐全的开发者体验。

UIKit是基于Objective-C语言开发和发布的。即使到了今天,你在为iOS平台开发时也会经常碰到Objective-C代码。越来越多的开发者开始使用Swift,但UIKit框架仍然是以Objective-C为基础构建的。Objective-C是一种建立在C语言之上的语言,为C语言带来了类似Smalltalk的面向对象编程。Objective-C在苹果公司恢复和推出iPhone的过程中发挥了非常重要的作用。www.jianshu.com/p/ae3ab9fdc…

UIView和CALayer体验了职责分离的设计思想,其中UIView负责触摸事件的处理,而CALayer负责渲染层。之所以这样设计,是因为Mac等系统没有使用UIKit,而是使用了AppKit,但是底层渲染都使用了CoreAnimation

在UIView创建的同时,系统会创建一个layer绑定到UIView的属性上,被称为backing layer。 当我们修改UIView的圆角、边框等属性时,其实是UIView封装了Layer的修改方法,最终的渲染实现还是在Layer上。 而响应链等触摸响应的能力,则是由UIView来实现(当然,底层还是依赖Layer的-containsPoint:和-hitTest:等方法)

3.2 SwiftUI

SwiftUI是苹果生态系统中一个相对较新的部分。我说 "相对 "是因为它是在2019年随iOS SDK的第13版首次发布的。但SwiftUI已经成熟到如此程度,以至于它现在是许多应用程序开发人员的可行选择,特别是在针对iOS 14+甚至15+时。然而,iOS 13上的SwiftUI有一系列特定的限制,减缓了iOS应用开发者的采用。

SwiftUI 的核心理念是使用声明式语法来描述用户界面的组件和它们之间的关系,而不是传统的命令式编程方式。

SwiftUI是一个UI框架,与UIKit相比,它采用了一种更加反应式的方法来开发你的UI。在UIKit中,布局你的视图是非常明确的。对窗口和UI的调整需要计算和更新尺寸,或者从视图层次中添加和删除视图。

另一方面,SwiftUI更强调定义你想在屏幕上看到的内容。当屏幕上的内容需要改变时,你定义新的状态,如果一切按计划进行,SwiftUI将计算所有的差异并自动更新显示给终端用户的表现。你可以对屏幕上发生的事情进行细粒度的控制。但即便如此,实际的渲染还是来自于对UI底层状态的修改。UIKit的工作方向正好相反;作为一个开发者,你需要检测什么已经改变,并主动更新整个UI以反映这个新的状态。

SwiftUI在很大程度上依赖于Swift中存在的语言功能。这一点很重要,因为苹果首先需要向Swift语言添加特定的功能,以便能够像现在这样创建SwiftUI。苹果在2014年公开发布了Swift,并在这些年里对该语言进行了大量更新。这些更新有些是小的,有些是比较重要的。问问任何在Swift早期从事iOS工作的开发者,他们可以分享一些关于在Swift版本之间迁移的故事以及其固有的挑战/

3.3 声明式和命令式的编程方式区别,以SwiftUI和UIkit为例

声明式编程(Declarative Programming)和命令式编程(Imperative Programming)是两种不同的编程范式。以SwiftUI(声明式)和UIKit(命令式)为例,我们可以探讨它们之间的区别:

  1. 编程方式

   - 声明式(SwiftUI):你只需声明你想要的界面应该是什么样子,系统会根据你的声明去构建和更新UI。例如,你声明一个按钮应该显示什么文本,而不是告诉系统如何显示这个按钮。在声明式编程范式中,开发者只需声明界面应该呈现的内容和状态,而不需要详细说明如何实现这些界面。这种方式简化了代码,使其更易于理解和维护。开发者描述“什么”而非“怎么做”。

   - 命令式(UIKit):你需要告诉系统如何去步骤地更新UI,例如通过一系列的命令来创建一个按钮,设置它的属性,然后将它添加到视图中。

  1. 代码的可读性和简洁性

   - SwiftUI:代码更加简洁,可读性强。因为它更加关注于UI应该是什么样子,而不是如何去实现它。

   - UIKit:代码可能会更加繁琐,因为需要详细描述UI的创建和更新过程。

  1. 状态管理

   - SwiftUI:通过数据绑定和状态管理的方式,当数据变化时,UI会自动更新。这使得状态管理变得更加简单和直观。

   - UIKit:需要手动管理数据和UI之间的同步,当数据变化时,需要编写额外的代码来更新UI。

  1. 学习曲线

   - SwiftUI:对于熟悉声明式编程的开发者来说,学习曲线较为平缓。但对于习惯了命令式编程的开发者,可能需要一段时间来适应。

   - UIKit:对于习惯了命令式编程的开发者来说,学习曲线较为平缓。但对于新手来说,需要理解更多关于UI组件的创建和管理的细节。

  1. 跨平台开发

   - SwiftUI:由于其声明式的特性,更容易被用于跨平台开发。

   - UIKit:主要用于iOS和tvOS开发,对于跨平台开发支持不如SwiftUI。

总结来说,SwiftUI提供了一种更加简洁、高效的方式来构建UI,特别是在状态管理和跨平台开发方面有明显优势。而UIKit则给予开发者更多的控制权,适用于需要精细控制UI的场景。选择哪一种,取决于项目需求和开发者的偏好。

SwiftUI 的特点
简洁的语法:SwiftUI 使用简洁的 Swift 语法,使得用户界面代码更加直观和易于编写。
实时预览:SwiftUI 配合 Xcode 提供的 Canvas 功能,可以实时预览界面设计,即时看到代码更改的效果。
数据驱动:SwiftUI 强调数据驱动的界面设计,使用 @State@Binding@ObservedObject 等属性包装器来管理状态,界面会自动响应状态变化。
跨平台:使用相同的 SwiftUI 代码基础,可以构建多个平台的应用,极大地提高了代码的复用性。
import SwiftUI
struct ContentView: View {
    @State private var message = "Hello, world!"

    var body: some View {
        VStack {
            Text(message)
                .font(.largeTitle)
            Button("Click Me") {
                self.message = "Button Clicked"
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
        }
    }
}

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

3.4 OC和Swift 枚举的区别

在 iOS 开发中,枚举(Enumerations)是一种常用的类型,用于定义一组有关联的值。Swift 的枚举提供了强大的功能,比 Objective-C 中的枚举更加灵活和安全。

Objective-C 中的枚举

Objective-C 中的枚举通常是基于整数的,使用 enum 关键字定义:

typedef enum { DirectionNorth, DirectionSouth, DirectionEast, DirectionWest } Direction;

或者使用更现代的语法,指定底层类型:

typedef NS_ENUM(NSInteger, Direction) { DirectionNorth, DirectionSouth, DirectionEast, DirectionWest };

这种枚举的值实际上是整数(默认从 0 开始),并且它们不具备类型安全性和自描述性。

Swift 中的枚举

Swift 的枚举更加强大和灵活,它不仅可以是整数类型,还可以是字符串、字符或者是一个完全自定义的类型。Swift 的枚举还支持关联值(Associated Values)、原始值(Raw Values)和方法。

基本枚举

enum Direction { case north case south case east case west }

带有原始值的枚举

enum Direction: String { case north = "North" case south = "South" case east = "East" case west = "West" }

带有关联值的枚举

enum Barcode { case upc(Int, Int, Int, Int) case qrCode(String) }

枚举的方法

enum Direction { case north, south, east, west func description() -> String { switch self { case .north: return "向北" case .south: return "向南" case .east: return "向东" case .west: return "向西" } } }

4、扩展

4.1 点击事件和手势的响应链是如何传递的,有什么区别

点击事件和手势的响应链传递在 iOS 中是通过响应者链(Responder Chain)机制来实现的,但它们在传递过程中存在一些区别。

点击事件的响应链传递:

  1. 触摸事件开始:当用户触摸屏幕时,系统会生成一个触摸事件(TouchEvent)。

  2. 寻找第一响应者:系统会将这个触摸事件传递给应用程序的主窗口(Window),主窗口会通过 **hitTest:withEvent:** 方法确定哪个视图最适合处理这个事件。

  3. 事件传递:一旦确定了最适合处理事件的视图,事件就会沿着视图层次结构向下传递,直到找到能够处理该事件的视图。这个视图成为事件的第一响应者。

  4. 事件处理:第一响应者会通过 touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:touchesCancelled:withEvent: 方法来处理事件。

  5. 事件冒泡:如果第一响应者不能处理该事件,事件会沿着响应者链向上传递,直到找到能处理该事件的视图或控制器。

点击展开内容

在iOS中,触摸事件的处理遵循一个从上至下的传递机制,即事件首先由应用程序的主window接收,然后沿着视图层次结构向下传递,直到找到一个能够处理该事件的子视图。 因此,触摸事件是从主window开始寻找子视图的,而不是由子视图直接接收。

事件传递过程

  1. 触摸事件接收:当用户触摸屏幕时,系统首先将触摸事件发送给应用程序的主window

  2. 事件传递window会将事件传递给最顶层的视图,然后这个视图会将事件传递给它的子视图,这个过程会一直持续下去,直到找到一个能够处理该事件的视图。

  3. hit-testing:每个视图都会通过hitTest:withEvent:方法来检查触摸点是否在其坐标空间内。如果是,该视图会继续将事件传递给它的子视图,直到找到最具体的子视图(即触摸点直接落在该视图上,且该视图能够处理触摸事件)。

  4. 事件处理:一旦找到能够处理事件的视图,该视图会通过 touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:等方法来响应和处理事件。

事件响应者链

如果一个视图决定不处理一个触摸事件,该事件可以沿着响应者链(responder chain)向上传递,直到找到能够处理该事件的响应者。响应者链是由当前视图开始,通过nextResponder方法连接起来的一系列响应者(UIView、UIViewController、UIApplication等)。

总结

触摸事件在iOS中的处理机制是从主window开始,沿着视图层次结构向下寻找能够处理该事件的最具体的子视图。这个过程涉及到hit-testing和事件传递,确保了触摸事件能够被正确地识别和处理。如果最具体的视图选择不处理该事件,事件会沿着响应者链向上传递,直到找到一个愿意处理它的响应者。

手势的响应链传递:

  1. 手势识别开始:当用户在屏幕上做出特定的手势时,系统会尝试识别这个手势。

  2. 关联视图:手势识别器(GestureRecognizer)被关联到一个特定的视图上。当手势发生在这个视图上时,手势识别器会开始工作。

  3. 手势识别:手势识别器会根据其配置来识别特定的手势。如果识别成功,它会调用与之关联的目标-动作(target-action)。

  4. 事件拦截手势识别器可以拦截触摸事件,阻止这些事件继续沿着响应链传递。这意味着,一旦手势被识别,与手势识别器关联的视图可能不会接收到标准的触摸事件消息(如 touchesBegan:withEvent:)。

区别:

  • 传递机制:点击事件是通过视图层次结构向下传递,直到找到第一响应者;而手势识别是基于手势识别器关联的视图进行的,手势识别器可以直接拦截事件。

  • 事件处理:点击事件通常由视图的触摸事件处理方法(如 touchesBegan:withEvent:)处理;手势则是由手势识别器的目标-动作机制处理。

  • 事件拦截:手势识别器可以拦截触摸事件,防止它们被进一步传递;而点击事件的传递则更多依赖于视图层次和响应者链的结构。

总的来说,点击事件和手势在 iOS 中都是通过响应者链来传递的,但手势识别器提供了更多的控制能力,允许开发者拦截和处理特定的用户交互。

4.2 pointInside:withEvent:hitTest:withEvent:

它们属于 UIView 类。这两个方法通常用于确定触摸事件的目标视图。

  1. pointInside:withEvent: 方法用于判断一个点是否在当前视图的边界内。它接收一个点(CGPoint)和一个事件(UIEvent),如果这个点在视图的边界内,就返回 YES,否则返回 NO。这个方法通常用于自定义视图时改变视图的触摸响应区域。

示例代码:


- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {

CGRect bounds = self.bounds;

// 扩大bounds的范围,使得即使触摸点不在视图的标准边界内,也能响应

CGFloat widthDelta = MAX(45.0 - bounds.size.width, 0);

CGFloat heightDelta = MAX(45.0 - bounds.size.height, 0);

bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);

return CGRectContainsPoint(bounds, point);

}

  1. hitTest:withEvent: 方法用于确定哪个子视图(如果有的话)应该接收触摸事件。它接收一个点(CGPoint)和一个事件(UIEvent),然后返回最合适接收该事件的视图。如果视图不接受触摸事件或点不在视图内,它会返回 nil。这个方法通常用于视图层次较复杂的情况,用于确定具体哪个子视图应当处理触摸事件。

示例代码:


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

// 调用父类的hitTest:withEvent:来确定触摸点是否在当前视图内

UIView *result = [super hitTest:point withEvent:event];

if (result) {

// 如果在当前视图内,返回最合适的子视图

return result;

}

// 如果不在当前视图内,返回nil

return nil;

}

总结:pointInside:withEvent: 主要用于判断触摸点是否在视图内,而 hitTest:withEvent: 用于确定具体哪个视图应当处理触摸事件。这两个方法在自定义视图和处理复杂的视图层次时非常有用。

4.3 分享能力

在iOS开发中,分享能力是指应用程序能够将内容(如文本、图片、链接等)分享到其他应用或服务中的功能。iOS提供了多种方式来实现分享功能,主要通过**UIActivityViewController**来实现,同时也支持通过自定义分享扩展来扩展分享功能。

### 使用UIActivityViewController实现分享
`UIActivityViewController`是一个用于展示标准服务的视图控制器,如发送短信邮件复制到剪贴板保存图片到相册分享到社交媒体等使用`UIActivityViewController`可以非常方便地实现内容的分享

#### 示例代码:
```swift

import UIKit

class ViewController: UIViewController {

    @IBAction func shareButtonTapped(_ sender: UIButton) {

        // 要分享的内容

        let text = "这是要分享的文本内容"

        let image = UIImage(named: "exampleImage.png")

        let url = URL(string: "https://example.com")

        

        // 创建UIActivityViewController

        let activityViewController = UIActivityViewController(activityItems: [text, image!, url!], applicationActivities: nil)

       
        // 排除不需要的分享服务

        activityViewController.excludedActivityTypes = [.addToReadingList, .postToFlickr]

        

        // 在iPad上,需要配置popoverPresentationController

        if let popoverController = activityViewController.popoverPresentationController {

            popoverController.sourceView = self.view

            popoverController.sourceRect = sender.frame

        }

        

        // 展示分享视图控制器

        self.present(activityViewController, animated: true, completion: nil)

    }

}

```

在这个示例中,当用户点击分享按钮时,会弹出一个包含多种分享选项的`UIActivityViewController`开发者可以通过`activityItems`参数传递一个或多个要分享的对象,也可以通过`excludedActivityTypes`属性来排除不需要的分享服务

### 自定义分享扩展

除了使用`UIActivityViewController`提供的标准分享服务外,iOS还允许开发者通过创建分享扩展(Share Extension)来实现自定义的分享功能分享扩展可以让用户在其他应用中通过系统分享菜单将内容分享到你的应用

创建分享扩展涉及到在Xcode中配置扩展目标(extension target),实现扩展的主要逻辑,以及配置扩展的信息属性列表(Info.plist)以声明支持的内容类型等由于涉及较多的步骤,具体实现可以参考官方文档和指南

### 总结

iOS的分享能力主要通过`UIActivityViewController`来实现,它支持多种标准的分享服务,并且可以通过自定义分享扩展来扩展分享功能
这些工具和API为开发者提供了强大的分享能力,使得应用程序能够方便地与其他应用和服务进行内容分享

4.4 打开其他应用

在iOS开发中,打开其他应用是一个常见的需求,可以通过URL Schemes和Universal Links两种方式来实现。这两种技术在用途和实现方式上有所不同。

1、URL Schemes

URL Schemes是一种基于URL的方式,允许应用程序相互调用。每个应用可以定义自己的URL Scheme(如myapp://),其他应用可以通过这个Scheme来启动它,并且可以传递一些参数。

if let url = URL(string: "myapp://some/path?query=parameter") {
    if UIApplication.shared.canOpenURL(url) {
        UIApplication.shared.open(url, options: [:], completionHandler: nil)
    }
}
// 在这个例子中,首先检查设备上是否安装了对应的应用(即是否可以打开这个URL),如果可以,则使用UIApplication.shared.open方法来打开它。

2、Universal Links

iOS 9及以后版本引入的一种技术,允许通过标准的HTTP或HTTPS链接来启动应用。与URL Schemes不同,Universal Links使用的是普通的网址,当用户点击这样的链接时,系统会首先尝试打开对应的应用,如果应用未安装,则会回退到Safari打开这个链接对应的网页。

Universal Links的设置比URL Schemes复杂,需要在应用的开发者账号中配置Associated Domains,并且要求网站主机提供一个特定的JSON文件(apple-app-site-association)来验证链接与应用的关联。

Universal Links的优点:

  • 更安全,因为它依赖HTTPS和验证过程。
  • 提供更好的用户体验,因为它可以直接打开应用而不是重定向到Safari,如果应用未安装,还可以优雅地回退到网页。
  • 支持传递更丰富的信息给应用。

URL Schemes与Universal Links的区别

  • 安全性:Universal Links比URL Schemes更安全,因为它依赖于HTTPS和验证过程。
  • 用户体验:Universal Links可以提供更连贯的用户体验,因为它可以直接打开应用,如果应用未安装,则可以回退到网页,而URL Schemes可能会导致用户看到一个错误提示(如果应用未安装)。
  • 配置:Universal Links的配置比URL Schemes复杂,需要网站的支持和额外的配置。
  • 兼容性:URL Schemes在所有iOS版本上都受支持,而Universal Links仅在iOS 9及以后版本中受支持。

总的来说,Universal Links提供了一种更安全、用户体验更好的方式来实现应用间的跳转,但它的配置和使用要求更高。URL Schemes则更简单易用,但在安全性和用户体验上有所不足。开发者可以根据自己的需求选择合适的技术。

4.5 实况图

点击展开内容

在Objective-C中实现实况图(即实时更新的图表),通常需要借助第三方图表库,如Core Plot、Charts等。这里以使用Charts库为例,介绍如何在Objective-C项目中实现一个简单的实况图。

步骤 1: 安装Charts库

首先,确保你的项目已经安装了Charts库。如果你使用CocoaPods管理项目依赖,可以在Podfile中添加如下代码:


pod 'Charts'

然后在终端中运行pod install命令来安装库。

步骤 2: 导入Charts库

在你打算使用Charts库的Objective-C文件中导入头文件:


#import <Charts/Charts.h>

步骤 3: 创建图表视图

在你的视图控制器中,创建一个LineChartView实例,并添加到视图中:


@interface ViewController ()

@property (strong, nonatomic) LineChartView *lineChartView;

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

self.lineChartView = [[LineChartView alloc] initWithFrame:self.view.bounds];

[self.view addSubview:self.lineChartView];

// 配置图表数据

[self setupChartData];

}

- (void)setupChartData {

NSMutableArray<ChartDataEntry *> *values = [[NSMutableArray alloc] init];

// 模拟数据

for (int i = 0; i < 20; i++) {

double val = arc4random_uniform(100);

[values addObject:[[ChartDataEntry alloc] initWithX:i y:val]];

}

LineChartDataSet *set1 = [[LineChartDataSet alloc] initWithEntries:values label:@"DataSet 1"];

LineChartData *data = [[LineChartData alloc] initWithDataSet:set1];

self.lineChartView.data = data;

}

@end

在这个例子中,我们创建了一个LineChartView实例,并在setupChartData方法中生成了一些模拟数据来填充图表。LineChartDataSet用于表示一组数据,而LineChartData则用于将数据集绑定到图表上。

步骤 4: 更新图表数据

为了实现实况图的效果,你需要定期更新图表的数据。这可以通过设置一个定时器,并在定时器的回调方法中更新数据来实现:


- (void)startUpdatingChart {

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateChartData) userInfo:nil repeats:YES];

}

- (void)updateChartData {

// 更新数据的逻辑

// 可以添加新的数据点到数据集中,并从图表中移除旧的数据点

// 然后调用[self.lineChartView notifyDataSetChanged]来刷新图表

}

updateChartData方法中,你可以根据实际需求添加新的数据点,并移除旧的数据点,然后调用[self.lineChartView notifyDataSetChanged]来通知图表视图数据已更新,需要刷新显示。

总结

通过上述步骤,你可以在Objective-C项目中使用Charts库实现一个简单的实况图。根据实际需求,你可能还需要对图表进行更多的配置和定制,如调整图表的样式、格式化坐标轴标签等。Charts库提供了丰富的配置选项,可以满足大多数图表绘制需求。

5、LLVM

5.1 LLVM使用

LLVM是一个开源的编译器基础设施项目,全称为Low Level Virtual Machine(低级虚拟机)。它为多种编程语言提供了一套全面的工具链,包括编译器、链接器、调试器等。LLVM的设计目标是实现编译时、链接时、运行时以及“闲置时”的代码优化,支持多种编程语言和多种目标平台。

核心特性

  1. 模块化设计:LLVM采用模块化的设计,使得它可以被用作多种用途的编译技术的后端。这意味着开发者可以为自己的语言或工具选择LLVM作为后端,利用LLVM提供的优化和代码生成能力。

  2. 中间表示(Intermediate Representation,IR):LLVM的核心是一种独立于语言和架构的中间表示(IR),这种IR设计为SSA(Static Single Assignment)形式,便于进行各种编译时优化。LLVM IR既可以用作编译器的输出,也可以作为其他工具的输入。

  3. 跨平台:LLVM支持多种目标架构,包括但不限于x86、ARM、PowerPC和MIPS等。这使得基于LLVM的语言和工具能够轻松地跨平台工作。

  4. 前端多样性:LLVM支持多种编程语言的前端,例如Clang(C/C++/Objective-C编译器)、Swift编译器等。这些前端可以将特定语言的源代码转换为LLVM IR。

  5. 强大的优化框架:LLVM提供了一系列的编译时优化技术,如函数内联、死代码删除、循环优化等,以及链接时优化(LTO)。

  6. JIT编译支持:LLVM提供了JIT(Just-In-Time)编译的支持,允许代码在运行时被编译和优化,适用于需要即时编译的应用场景,如脚本语言的执行环境。

应用场景

  • 编程语言的实现:许多现代编程语言,如Rust和Swift,都选择LLVM作为其编译器后端。

  • 特定领域语言(DSL)的开发:LLVM的模块化设计使其成为开发特定领域语言的理想选择。

  • 静态和动态分析工具:基于LLVM IR,开发者可以构建各种代码分析工具,如静态分析器、性能分析器等。

  • 跨平台工具链:LLVM支持多种目标架构,使得基于LLVM的工具链能够轻松实现跨平台编译。

总结

LLVM是一个强大的编译器基础设施,提供了丰富的特性和工具,支持从前端语言处理到后端代码生成和优化的全过程。它的模块化和跨平台特性使得LLVM不仅适用于传统的编译器开发,也适用于各种编程语言和工具的实现,以及性能优化和代码分析等多种场景。

6、视图渲染

视图渲染是一个将视图数据转换为用户界面的过程。在iOS中,视图渲染过程主要涉及UIKit和Core Animation框架,整个过程可以大致分为以下几个步骤:

1. 布局计算(Layout Calculation)

  • 布局阶段:首先,系统需要计算视图的布局。这包括确定视图的大小和位置,这个过程通常是通过Auto Layout或者手动设置frame来完成的。在这个阶段,**layoutSubviews**方法会被调用,以便为子视图布局。

2. 图层树的构建(Layer Tree Construction)

  • 图层树构建:每个UIView都有一个对应的CALayer(Core Animation图层)。UIView主要负责事件处理,而CALayer负责内容的显示。UIView会将渲染内容的任务委托给它的layer。在布局计算完成后,会根据视图的层级关系构建一个图层树。

3. 绘制(Drawing)

  • 绘制阶段:在需要时,图层会要求它的代理(通常是UIView)绘制内容。这通常是通过调用draw(_:)drawInRect:方法来完成的。在这个阶段,可以使用Core Graphics或者UIKit的绘图函数来绘制图层的内容。

4. 栅格化(Rasterization)

  • 栅格化绘制操作生成的是矢量图形,而屏幕显示的是位图。因此,绘制的内容需要被栅格化(转换为位图)。这个过程通常是在后台线程中完成的。

5. 合成(Composition)

  • 图层合成:一旦所有的图层都被栅格化Core Animation会根据图层树的结构将它们合成到一起。如果有必要,会应用各种图层属性(如透明度、变换等)。

6. 显示(Display)

  • 显示到屏幕合成后的最终图像会被提交给GPU,由GPU完成最后的渲染,并将结果显示到屏幕上。这个过程涉及到将位图数据复制到帧缓冲区,然后由视频硬件显示出来。

优化

  • 离屏渲染:在某些情况下,如使用圆角、阴影等效果时,图层的合成可能需要在离屏缓冲区进行,这会增加额外的性能开销。合理使用这些特性可以避免不必要的离屏渲染,提高渲染效率。

  • 重用和延迟加载:通过重用视图和延迟加载不可见内容,可以减少渲染负担,提高应用性能。

整个视图渲染过程是一个从布局计算到最终显示到屏幕的复杂过程,涉及到多个系统组件的协作。理解这个过程有助于开发者编写更高效的代码,优化应用的性能。

Q:既然图像渲染都是走的layer层,那为什么不建议对layer操作,而是对Uiview。

在iOS开发中,UIView和CALayer是两个紧密相关的类,它们共同负责视图的显示和动画。虽然图像渲染确实主要通过CALayer层进行,但建议开发者主要通过UIView进行操作,原因如下:

1. 职责分离

UIView主要负责处理用户交互和视图的布局,而CALayer主要负责视图的渲染和动画。这种职责分离使得代码更加清晰和模块化。通过UIView,开发者可以更方便地处理触摸事件、手势识别等用户交互,而CALayer则专注于视觉效果的呈现。

2. 简化开发

UIView提供了许多便捷的方法和属性,使得视图的创建、布局和更新变得更加简单。例如,UIView的frameboundscenter等属性可以直接用于视图的定位和大小调整,而CALayer则需要通过frameboundspositionanchorPoint等属性进行更复杂的计算。

3. 自动布局

UIView与Auto Layout系统紧密集成,可以方便地使用约束来定义视图的位置和大小。而CALayer则没有直接支持Auto Layout,需要手动计算和更新布局。

4. 事件处理

UIView继承自UIResponder,可以响应触摸事件和其他用户交互事件。而CALayer不具备这种能力,它只能处理与显示相关的任务。

5. 内存管理

在Objective-C中,UIView的内存管理是通过引用计数(ARC)自动处理的,而CALayer的内存管理则需要开发者手动处理。虽然这在Swift中不再是问题,但UIView的自动内存管理仍然简化了开发过程。

6. 动画支持

虽然CALayer提供了强大的动画支持,但UIView通过封装CALayer的动画功能,提供了更简单易用的动画API。例如,UIView的animate(withDuration:animations:)方法可以方便地创建和执行动画。

7. 调试和维护

由于UIView提供了更高层次的抽象,使用UIView进行开发通常会使代码更易于调试和维护。UIView的属性和方法更符合开发者的直觉,减少了出错的可能性。

综上所述,虽然图像渲染确实主要通过CALayer层进行,但建议开发者主要通过UIView进行操作,以简化开发过程、提高代码的可读性和可维护性,并充分利用UIView提供的便捷功能和集成支持。

6.2 离屏渲染--离屏缓冲区-帧缓冲的对象(FBO, FramBuffer Object)

6.4 异步渲染

异步渲染是一种在前端开发中常用的技术,特别是在构建复杂的单页面应用(SPA)时。它允许浏览器在加载和渲染页面的同时,执行其他任务,如数据获取、图片加载等,从而提高应用的性能和用户体验。

异步渲染的原理

异步渲染的核心是将渲染任务分解成小的、可管理的单元,并在浏览器的主线程空闲时执行这些任务,而不是一次性完成所有渲染工作。这种方式可以避免长时间的渲染任务阻塞主线程,导致页面卡顿。

异步渲染的实现

  1. 使用Promise和async/await:这是实现异步操作的基本方式。通过Promise或async/await,可以在数据获取完成后再更新DOM,而不会阻塞页面的其他操作。

  2. 虚拟DOM(Virtual DOM):许多现代前端框架(如React、Vue)使用虚拟DOM来实现异步渲染。虚拟DOM允许框架在内存中构建一个DOM树的副本,当数据变化时,框架会计算出虚拟DOM树的差异,并异步地将这些差异应用到实际的DOM树上,从而避免了不必要的DOM操作,提高了性能。

  3. Web Workers:Web Workers提供了一种在后台线程执行脚本的能力,而不会影响主线程的性能。虽然Web Workers不能直接操作DOM,但它们可以用来执行计算密集型或异步任务,如数据处理、大量数据的排序等,然后将结果发送回主线线程进行渲染。

  4. 请求动画帧(requestAnimationFrame)requestAnimationFrame是浏览器提供的一个API,允许开发者告诉浏览器在下一次重绘之前调用指定的回调函数,用于执行动画更新。这个API可以用来平滑地实现动画和页面渲染,因为它会在浏览器准备好渲染新帧时被调用,从而最大限度地减少布局抖动和重绘。

  5. 时间切片(Time Slicing):React 16引入了Fiber架构,其中一个关键特性就是时间切片。时间切片允许React将渲染工作分割成小块,并根据浏览器的空闲时间来调度这些任务的执行。这样,即使是大型组件的渲染也不会阻塞主线程,提高了应用的响应性。

总结

异步渲染通过将渲染任务异步化,使得页面可以在加载重要内容的同时,继续处理用户的交互,从而提高了用户体验。现代前端框架和API提供了多种实现异步渲染的方法,开发者可以根据应用的需求和特点选择合适的技术方案。

6.4 图片加载过程

juejin.cn/post/684490…

1. 加载图片

  • 从磁盘中加载一张图片;
  • 然后将生成的 UIImage 赋值给 UIImageView ;
  • 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  • 分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;

2. 图片解码(解压)

  • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;

3. 图片渲染

  • Core Animation 中CALayer使用解压(解码)的位图数据渲染 UIImageView 的图层
  • CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;
  • GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);
  • 从帧缓存区中渲染到屏幕上;

image.jpeg

顶点着色器和片元着色器

顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)是图形渲染管线中的两个重要阶段,它们在现代图形API(如OpenGL、DirectX)中用于实现可编程渲染管线。

  1. 顶点着色器
  • 顶点着色器是渲染管线的第一个可编程阶段。它以顶点为单位进行工作,每个顶点调用一次。

  • 主要职责是处理顶点数据(如位置、颜色、纹理坐标等),并进行顶点位置的变换计算,输出变换后的顶点位置给下一个阶段。

  • 可以用来实现各种顶点级别的效果,如顶点动画、几何变形等。

  1. 片元着色器
  • 片元着色器(有时也称为像素着色器)是渲染管线中的一个后期可编程阶段,它以片元(或像素)为单位进行工作,每个片元调用一次。

  • 主要职责是计算最终像素的颜色值,包括纹理采样、光照计算、颜色混合等。

  • 可以用来实现各种像素级别的效果,如贴图、光照、阴影、反射等。

两者的关系和区别:

  • 关系:顶点着色器处理完顶点后,图形管线会进行图元装配(如三角形装配),然后进行光栅化,将图元转换为片元,最后片元着色器对每个片元进行处理,输出最终的像素颜色。

  • 区别:顶点着色器主要处理顶点级别的操作,如顶点变换;而片元着色器主要处理像素级别的操作,如纹理映射和光照效果。两者在渲染管线中承担不同的角色,但都是可编程着色器,允许开发者自定义着色器代码来实现特定的图形效果。

在现代图形应用中,顶点着色器和片元着色器的灵活性和强大功能使得它们成为实现复杂渲染效果的基石。

这其中有个关键步骤是图片解码。那为什么要解码呢,这是因为我们平常使用的图片一般为了节约空间都会经过一些压缩算法进行封装,而使用时屏幕要精确的渲染到每个像素点,这就需要把压缩的图片解码展开,便于系统处理。