1. xib
的注意点
i: 是什么
xib
文件是 Interface Builder 的文件格式,用于可视化地创建 UIView
及其子类的界面布局。
ii: 为什么
使用 xib
可以提高界面开发的效率,减少代码量,方便界面调整和维护。
iii: 怎么做
-
File's Owner
设置:-
File's Owner
代表xib
文件的拥有者,通常是UIViewController
或自定义UIView
子类。 - 在
xib
中,你需要将File's Owner
的Class
设置为对应的类名。 - 通过
File's Owner
,你可以在xib
中连接IBOutlet
和IBAction
。
-
-
IBOutlet
和IBAction
连接:- 在代码中声明
IBOutlet
和IBAction
,然后在xib
中将它们连接到对应的 UI 元素。 - 确保连接正确,避免出现
nil
或崩溃。
- 在代码中声明
-
约束设置:
- 使用 Auto Layout 约束来定义 UI 元素的位置和大小。
-
xib
中的约束会影响到运行时界面的布局。 - 注意约束的优先级和冲突,避免出现布局错误。
-
加载
xib
:- 使用
UINib
类加载xib
文件。 - 可以使用
instantiateWithOwner:options:
方法来加载xib
文件,并将File's Owner
设置为当前对象。
- 使用
-
避免过度使用:
-
xib
适合于创建简单的、可复用的 UI 元素。 - 对于复杂的界面布局,使用代码或者 Storyboard 可能更灵活。
-
示例:
// 自定义 UIView 子类
class MyView: UIView {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var contentTextView: UITextView!
override init(frame: CGRect) {
super.init(frame: frame)
loadViewFromNib()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
loadViewFromNib()
}
private func loadViewFromNib() {
let nib = UINib(nibName: "MyView", bundle: nil)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
}
// 在 UIViewController 中使用
class MyViewController: UIViewController {
@IBOutlet weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
myView.titleLabel.text = "Hello"
myView.contentTextView.text = "World"
}
}
2. 泛型的单例
i: 是什么
泛型单例是指使用泛型技术实现的单例模式,可以创建特定类型的单例对象。
ii: 为什么
使用泛型单例可以避免类型转换,提高代码的类型安全性和可复用性。
iii: 怎么做
class Singleton<T> {
static var shared: T {
struct Static {
static let instance = T.self as! T // 强制转换类型
}
return Static.instance
}
}
// 使用示例
class MyManager: Singleton<MyManager> {
func doSomething() {
print("MyManager is doing something")
}
}
MyManager.shared.doSomething()
3. 部分结果需要主线程稍微延迟一点时长为何不管这时长是 0.000000001 还是多少才是准确的
i: 是什么
在多线程编程中,我们经常需要在主线程更新 UI。但有时,即使使用 DispatchQueue.main.async
,UI 的更新也可能不是立即生效,而是会延迟一段时间。
ii: 为什么
这是因为主线程的 RunLoop 并不是一直处于运行状态,它会在处理完一个事件后进入休眠状态,等待下一个事件的到来。DispatchQueue.main.async
只是将任务提交到主线程的 RunLoop 中,但任务的执行仍然需要等待 RunLoop 被唤醒。
至于延迟时间,不管是 0.000000001
还是其他极短的时间,都无法保证 UI 的立即更新。因为 RunLoop 的唤醒时间是不确定的,它取决于系统的调度和主线程的繁忙程度。
要确保 UI 的准确更新,最佳做法是避免使用极短的延迟,而是依赖于 RunLoop 的正常调度。
iii: 怎么做
-
使用
DispatchQueue.main.async
:将 UI 更新的代码放到DispatchQueue.main.async
中,确保在主线程执行。 - 避免过度更新:减少 UI 的更新频率,避免在短时间内进行大量的 UI 操作。
-
使用
CADisplayLink
:对于需要高频率更新的 UI,可以使用CADisplayLink
,它会在屏幕刷新时执行回调,从而实现 UI 的平滑更新。
示例:
// 错误的做法
DispatchQueue.main.async {
Thread.sleep(forTimeInterval: 0.000000001) // 试图延迟极短的时间
myLabel.text = "Hello" // 无法保证立即更新
}
// 正确的做法
DispatchQueue.main.async {
myLabel.text = "Hello" // 依赖 RunLoop 的正常调度
}
4. 约束动画的生效更改
i: 是什么
约束动画是指通过修改约束的值来实现动画效果。
ii: 为什么
使用约束动画可以实现灵活、平滑的动画效果,而且可以方便地与其他动画进行组合。
iii: 怎么做
-
修改约束的
constant
属性:- 获取需要修改的约束对象。
- 修改约束的
constant
属性的值。 - 在动画块中使用
layoutIfNeeded()
方法,强制视图重新布局。
-
使用
UIView.animate(withDuration:animations:)
方法:- 将修改约束的代码放到
UIView.animate(withDuration:animations:)
方法的animations
闭包中。 - 设置动画的时长、延迟、阻尼等属性。
- 将修改约束的代码放到
示例:
// 修改约束实现动画
UIView.animate(withDuration: 0.5) {
self.myConstraint.constant = 100 // 修改约束的值
self.view.layoutIfNeeded() // 强制重新布局
}
5. 如何布局是安全的
i: 是什么
安全的布局是指在各种屏幕尺寸、设备方向和系统版本下都能正确显示的布局。
ii: 为什么
不安全的布局会导致界面显示错误、元素重叠、内容被截断等问题,影响用户体验。
iii: 怎么做
-
使用 Auto Layout:
- 使用 Auto Layout 约束来定义 UI 元素的位置和大小,而不是使用固定值。
- Auto Layout 可以根据屏幕尺寸和设备方向自动调整布局。
-
使用 Size Classes:
- 使用 Size Classes 来为不同的屏幕尺寸和设备方向定义不同的约束。
- Size Classes 可以让你更精细地控制布局。
-
使用 Stack View:
- 使用 Stack View 来管理一组 UI 元素的布局。
- Stack View 可以自动调整元素的位置和大小,简化布局代码。
-
避免硬编码:
- 避免在代码中使用固定的数值来定义 UI 元素的位置和大小。
- 使用相对值或 Auto Layout 约束来适应不同的屏幕尺寸。
-
测试不同设备:
- 在不同的设备和模拟器上测试你的应用,确保布局在各种情况下都能正确显示。
6. AppDelegate
的拦截如何做呢?
i: 是什么
AppDelegate
拦截是指在 AppDelegate
的方法中,对某些事件进行拦截和处理,从而改变应用的默认行为。
ii: 为什么
AppDelegate
拦截可以用于实现一些自定义的功能,例如:
- 自定义启动流程
- 处理推送通知
- 处理 URL Scheme
- 监控应用状态
iii: 怎么做
-
在
AppDelegate
的方法中添加代码:- 在需要拦截的方法中,添加你的自定义代码。
- 根据需要,可以调用或不调用
super
方法,以决定是否执行默认行为。
-
使用
NotificationCenter
:- 使用
NotificationCenter
来监听某些系统事件,例如:-
UIApplicationDidFinishLaunchingNotification
:应用启动完成 -
UIApplicationWillEnterForegroundNotification
:应用将要进入前台 -
UIApplicationDidEnterBackgroundNotification
:应用已经进入后台
-
- 使用
-
使用
Swizzling
:- 使用
Swizzling
来替换AppDelegate
的方法,从而实现更高级的拦截。 -
Swizzling
是一种危险的技术,需要谨慎使用。
- 使用
示例:
// AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 在应用启动完成后,添加自定义代码
print("应用启动完成")
// 可以根据 launchOptions 来处理不同的启动场景
if let notification = launchOptions?[.remoteNotification] as? [String: Any] {
// 处理推送通知
print("收到推送通知:\(notification)")
}
return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
// 在应用将要进入前台时,添加自定义代码
print("应用将要进入前台")
}
}
7. 异步渲染的机制
i: 是什么
异步渲染是指将 UI 渲染的任务放到后台线程执行,从而避免阻塞主线程,提高应用的响应速度。
ii: 为什么
在 UI 界面中,渲染是一个耗时的操作,如果将渲染任务放到主线程执行,会导致主线程阻塞,应用出现卡顿现象。使用异步渲染可以将渲染任务放到后台线程执行,从而避免阻塞主线程,提高应用的响应速度。
iii: 怎么做
-
使用
DispatchQueue
:- 使用
DispatchQueue.global(qos: .userInitiated).async
将渲染任务放到后台线程执行。 - 在后台线程完成渲染后,使用
DispatchQueue.main.async
将渲染结果更新到 UI 界面。
- 使用
-
使用
OperationQueue
:- 使用
OperationQueue
来管理渲染任务。 -
OperationQueue
可以设置最大并发数,控制后台线程的数量。
- 使用
-
使用
AsyncDisplayKit
:-
AsyncDisplayKit
是一个专门用于异步渲染的框架。 -
AsyncDisplayKit
可以自动将 UI 渲染的任务放到后台线程执行,并提供了一系列的优化技术,例如:- Text Kit 渲染
- 图像解码
- 布局计算
-
示例:
// 异步渲染图像
DispatchQueue.global(qos: .userInitiated).async {
// 在后台线程解码图像
let image = UIImage(named: "my_image")
DispatchQueue.main.async {
// 在主线程更新 UI
myImageView.image = image
}
}
8. 支持协议的泛型处理 self
Self
Type.Self
T.Type
区别
i: 是什么
在 Swift 中,self
、Self
、Type.Self
和 T.Type
都与类型和泛型有关,但它们在使用场景和含义上有所不同。
ii: 为什么
理解这些关键字的区别,可以让你更好地使用 Swift 的类型系统和泛型特性。
iii: 怎么做
-
self
:-
self
指的是当前类型的实例对象。 - 在实例方法中,
self
指的是调用该方法的实例对象。 - 在类型方法中,
self
指的是当前类型本身。
-
-
Self
:-
Self
指的是当前类型本身,通常用于协议中。 -
Self
可以用于定义协议的关联类型,或者作为协议方法的参数或返回值类型。 - 使用
Self
可以让协议具有更强的灵活性和类型安全性。
-
-
Type.Self
:-
Type.Self
指的是元类型,也就是类型的类型。 - 元类型可以用于获取类型的静态信息,例如:
- 类型的名称
- 类型的属性
- 类型的方法
-
-
T.Type
:-
T.Type
指的是泛型类型T
的元类型。 -
T.Type
可以用于在泛型函数或方法中获取泛型类型的静态信息。
-
示例:
// 协议中使用 Self
protocol Copyable {
func copy() -> Self
}
// 结构体实现 Copyable 协议
struct MyStruct: Copyable {
var name: String
func copy() -> MyStruct {
return MyStruct(name: self.name)
}
}
// 类中使用 Type.Self
class MyClass {
static func createInstance(type: MyClass.Type) -> MyClass {
return type.init()
}
required init() {
// required init 确保子类必须实现 init 方法
}
}
// 泛型函数中使用 T.Type
func createInstance<T>(type: T.Type) -> T {
return type.init() as! T
}
9. 归档等
i: 是什么
归档是指将对象的状态保存到文件中,以便在以后恢复对象。
ii: 为什么
归档可以用于实现数据的持久化,例如:
- 保存应用的用户设置
- 保存游戏的角色状态
- 保存网络请求的缓存数据
iii: 怎么做
-
使用
NSKeyedArchiver
和NSKeyedUnarchiver
:-
NSKeyedArchiver
用于将对象归档到文件中。 -
NSKeyedUnarchiver
用于从文件中解档对象。 - 要使用
NSKeyedArchiver
和NSKeyedUnarchiver
,需要让对象实现NSCoding
协议。
-
-
使用
PropertyListSerialization
:-
PropertyListSerialization
用于将NSArray
、NSDictionary
、NSString
、NSNumber
、NSDate
和Data
等类型的对象序列化为 Property List 格式的数据。 - Property List 格式的数据可以保存到文件中,也可以通过网络传输。
-
-
使用 Core Data:
- Core Data 是苹果提供的一个对象关系映射(ORM)框架。
- Core Data 可以将对象存储到 SQLite 数据库中。
示例:
// 对象实现 NSCoding 协议
class Person: NSObject, NSCoding {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
// 从归档文件中解档对象
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
age = aDecoder.decodeInteger(forKey: "age")
super.init()
}
// 将对象归档到文件中
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
}
}
// 归档和解档对象
let person = Person(name: "张三", age: 20)
// 获取文件路径
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let archiveURL = documentsDirectory.appendingPathComponent("person.archive")
// 归档对象
NSKeyedArchiver.archiveRootObject(person, toFile: archiveURL.path)
// 解档对象
let unarchivedPerson = NSKeyedUnarchiver.unarchiveObject(withFile: archiveURL.path) as? Person
10. 某些表视图的头部无法精准计算紊乱布局? //某些需要那 view 再套一层
i: 是什么
在 UITableView
中,如果表头视图(tableHeaderView
)的布局比较复杂,可能会出现无法精准计算高度,导致布局紊乱的问题。
ii: 为什么
这是因为 UITableView
在计算表头视图的高度时,可能会受到以下因素的影响:
- 约束冲突
- 约束优先级
- 视图层级
- 内容自适应
iii: 怎么做
-
使用 Autolayout 精确计算高度:
- 确保表头视图内部的各个子视图都有明确的约束,能够自适应内容的高度。
- 通过以下代码精确计算表头视图的高度:
let headerView = tableView.tableHeaderView!
headerView.translatesAutoresizingMaskIntoConstraints = false
// 设置宽度约束以匹配表格视图的宽度
headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
- 使用内容自适应:
确保表头视图内部的 UILabel
和 UITextView
等控件启用了内容自适应,即 numberOfLines = 0
。
-
使用
systemLayoutSizeFitting(_:)
方法:- 使用
systemLayoutSizeFitting(_:)
方法来计算表头视图的实际高度。 -
systemLayoutSizeFitting(_:)
方法会根据约束和内容自适应来计算视图的最佳大小。
- 使用
-
使用
estimatedSectionHeaderHeight
属性:- 设置
estimatedSectionHeaderHeight
属性为一个估算值。 -
UITableView
会根据估算值来初始化表头视图的高度,然后在实际渲染时再进行调整。
- 设置
-
使用
view
再套一层:- 将表头视图放到一个
UIView
中,然后将UIView
设置为tableHeaderView
。 - 这样做可以隔离表头视图的约束和
UITableView
的约束,避免约束冲突。
- 将表头视图放到一个
示例:
// 创建表头视图
let headerView = MyHeaderView()
// 将表头视图放到一个 UIView 中
let containerView = UIView()
containerView.addSubview(headerView)
// 设置 containerView 的约束
containerView.translatesAutoresizingMaskIntoConstraints = false
headerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: containerView.topAnchor),
headerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
headerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
headerView.widthAnchor.constraint(equalTo: containerView.widthAnchor)
])
// 将 containerView 设置为 tableHeaderView
tableView.tableHeaderView = containerView
// 计算 containerView 的高度
containerView.layoutIfNeeded()
let height = containerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
// 设置 containerView 的 frame
var frame = containerView.frame
frame.size.height = height
containerView.frame = frame
// 重新设置 tableHeaderView
tableView.tableHeaderView = containerView
11. URL
filepath
string
区别
i: 是什么
URL
、filepath
和 string
在 iOS 开发中都用于表示地址或路径,但它们在使用场景和含义上有所不同。
ii: 为什么
理解这些类型的区别,可以让你更好地处理文件、网络资源和字符串。
iii: 怎么做
-
URL
:-
URL
(Uniform Resource Locator) 用于表示网络资源或本地文件的地址。 -
URL
可以包含协议、主机名、路径、查询参数等信息。 -
URL
可以用于访问网络资源(例如:网页、图片、视频),也可以用于访问本地文件。 -
URL
是一个结构体,提供了许多方法用于解析和操作 URL 地址。
-
-
filepath
:-
filepath
通常指的是文件在文件系统中的路径。 -
filepath
可以是绝对路径,也可以是相对路径。 -
filepath
是一个字符串,可以直接用于访问文件。
-
-
string
:-
string
是一个通用的字符串类型,用于表示文本数据。 -
string
可以包含任何字符,包括 URL 地址和文件路径。
-
示例:
// URL
let url = URL(string: "https://www.example.com/image.jpg")
// filepath
let filepath = "/Users/username/Documents/file.txt"
// string
let string = "Hello, world!"
总结:
-
URL
用于表示网络资源或本地文件的地址,是一个结构体。 -
filepath
用于表示文件在文件系统中的路径,是一个字符串。 -
string
是一个通用的字符串类型,可以包含任何字符。
12. 异步流的操作问题
i: 是什么
异步流是指在异步环境中处理数据流的技术。
ii: 为什么
在处理大量数据或需要进行耗时操作时,使用异步流可以避免阻塞主线程,提高应用的响应速度。
iii: 怎么做
-
使用
Combine
框架:-
Combine
是苹果提供的一个响应式编程框架。 -
Combine
可以用于处理异步事件和数据流。 -
Combine
提供了Publisher
、Subscriber
和Operator
等概念,可以方便地构建复杂的异步流。
-
-
使用
RxSwift
框架:-
RxSwift
是一个 ReactiveX 的 Swift 实现。 -
RxSwift
也可以用于处理异步事件和数据流。 -
RxSwift
提供了Observable
、Observer
和Operator
等概念,与Combine
类似。
-
-
使用
Async/Await
:-
Async/Await
是 Swift 5.5 引入的新的异步编程模型。 -
Async/Await
可以简化异步代码的编写,使其更易于理解和维护。
-
示例:
// 使用 Combine 框架
import Combine
let url = URL(string: "https://www.example.com/data.json")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [String: Any].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("完成")
case .failure(let error):
print("失败:\(error)")
}
}, receiveValue: { data in
print("数据:\(data)")
})
13. UIKit
界面底层渲染的瓶颈在哪
i: 是什么
UIKit
界面底层渲染的瓶颈是指在 UIKit
框架中,影响界面渲染性能的关键因素。
ii: 为什么
了解 UIKit
界面底层渲染的瓶颈,可以帮助你优化 UI 性能,提高应用的流畅度。
iii: 怎么做
-
CPU 计算:
-
UIKit
的布局计算、文本渲染、图像解码等操作都需要消耗 CPU 资源。 - 如果 CPU 资源不足,会导致 UI 渲染速度变慢。
-
-
GPU 渲染:
-
UIKit
的最终渲染需要依赖 GPU。 - 如果 GPU 压力过大,会导致帧率下降,出现卡顿现象。
-
-
离屏渲染:
- 离屏渲染是指将 UI 元素渲染到屏幕外的缓冲区中,然后再将缓冲区的内容渲染到屏幕上。
- 离屏渲染会消耗大量的 GPU 资源,降低渲染性能。
-
过度绘制:
- 过度绘制是指在同一像素上多次绘制 UI 元素。
- 过度绘制会浪费 GPU 资源,降低渲染性能。
-
图层混合:
- 图层混合是指将多个图层混合在一起进行渲染。
- 复杂的图层混合会消耗大量的 GPU 资源,降低渲染性能。
优化建议:
-
减少 CPU 计算:
- 避免在主线程进行复杂的计算。
- 使用缓存来减少重复计算。
- 使用 Instruments 工具来分析 CPU 占用情况。
-
减少 GPU 渲染:
- 避免使用离屏渲染。
- 减少过度绘制。
- 优化图层混合。
- 使用 Instruments 工具来分析 GPU 占用情况。
-
使用异步渲染:
- 将 UI 渲染的任务放到后台线程执行,避免阻塞主线程。
14. 如何处理提前拿到异步数据再渲染让用户无感
i: 是什么
在开发中,我们经常需要从网络或数据库加载数据,然后在 UI 界面中显示这些数据。如果数据加载需要一定的时间,用户可能会看到一个空白或加载中的界面,影响用户体验。
为了避免这种情况,我们可以使用一些技术来提前拿到异步数据,然后再渲染 UI 界面,让用户感觉不到数据加载的过程。
ii: 为什么
提前拿到异步数据可以提高应用的响应速度,改善用户体验。
iii: 怎么做
-
使用预加载:
- 在用户进入某个界面之前,提前加载该界面的数据。
- 可以使用
DispatchQueue.global(qos: .userInitiated).async
将预加载任务放到后台线程执行。
-
使用缓存:
- 将已经加载过的数据缓存到本地。
- 当用户再次进入该界面时,直接从缓存中读取数据,避免重新加载。
-
使用骨架屏:
- 在数据加载期间,显示一个骨架屏来模拟 UI 界面。
- 骨架屏可以给用户一种数据正在加载的错觉,避免用户感到空白或加载中的界面过于单调。
-
使用增量加载:
- 只加载用户当前需要的数据,而不是一次性加载所有数据。
- 当用户滚动或滑动界面时,再加载更多的数据。
示例:
// 预加载数据
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 检查数据是否已经缓存
if let cachedData = DataCache.shared.getData(forKey: "my_data") {
// 从缓存中读取数据
renderUI(withData: cachedData)
} else {
// 显示骨架屏
showSkeletonView()
// 异步加载数据
DispatchQueue.global(qos: .userInitiated).async {
let data = loadDataFromServer()
DispatchQueue.main.async {
// 隐藏骨架屏
hideSkeletonView()
// 渲染 UI 界面
renderUI(withData: data)
// 缓存数据
DataCache.shared.setData(data, forKey: "my_data")
}
}
}
}
15. 多个异步数据流的整合
i: 是什么
在实际开发中,我们经常需要从多个数据源加载数据,然后将这些数据整合在一起,才能渲染 UI 界面。
ii: 为什么
整合多个异步数据流可以提高应用的灵活性和可扩展性。
iii: 怎么做
-
使用
DispatchGroup
:-
DispatchGroup
可以用于等待多个异步任务完成。 - 当所有任务都完成后,
DispatchGroup
会通知主线程进行 UI 渲染。
-
-
使用
Combine
框架:-
Combine
提供了zip
、merge
和combineLatest
等操作符,可以用于整合多个Publisher
。
-
-
使用
RxSwift
框架:-
RxSwift
提供了zip
、merge
和combineLatest
等操作符,与Combine
类似。
-
-
使用
Async/Await
:-
Async/Await
可以使用async let
关键字来并发执行多个异步任务,然后使用await
关键字等待所有任务完成。
-
示例:
// 使用 Combine 框架
import Combine
let url1 = URL(string: "https://www.example.com/data1.json")!
let url2 = URL(string: "https://www.example.com/data2.json")!
let publisher1 = URLSession.shared.dataTaskPublisher(for: url1)
.map { $0.data }
.decode(type: [String: Any].self, decoder: JSONDecoder())
let publisher2 = URLSession.shared.dataTaskPublisher(for: url2)
.map { $0.data }
.decode(type: [String: Any].self, decoder: JSONDecoder())
Publishers.Zip(publisher1, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("完成")
case .failure(let error):
print("失败:\(error)")
}
}, receiveValue: { data1, data2 in
print("数据 1:\(data1)")
print("数据 2:\(data2)")
// 整合数据并渲染 UI 界面
let combinedData = combineData(data1, data2)
renderUI(withData: combinedData)
})
希望这些解答能够帮助您更好地理解 iOS 开发中的相关概念和技术。