UIKit一些细节需要了解深入的点

1. xib 的注意点

i: 是什么
xib 文件是 Interface Builder 的文件格式,用于可视化地创建 UIView 及其子类的界面布局。

ii: 为什么
使用 xib 可以提高界面开发的效率,减少代码量,方便界面调整和维护。

iii: 怎么做

  • File's Owner 设置:

    • File's Owner 代表 xib 文件的拥有者,通常是 UIViewController 或自定义 UIView 子类。
    • xib 中,你需要将 File's OwnerClass 设置为对应的类名。
    • 通过 File's Owner,你可以在 xib 中连接 IBOutletIBAction
  • IBOutletIBAction 连接:

    • 在代码中声明 IBOutletIBAction,然后在 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 中,selfSelfType.SelfT.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: 怎么做

  • 使用 NSKeyedArchiverNSKeyedUnarchiver

    • NSKeyedArchiver 用于将对象归档到文件中。
    • NSKeyedUnarchiver 用于从文件中解档对象。
    • 要使用 NSKeyedArchiverNSKeyedUnarchiver,需要让对象实现 NSCoding 协议。
  • 使用 PropertyListSerialization

    • PropertyListSerialization 用于将 NSArrayNSDictionaryNSStringNSNumberNSDateData 等类型的对象序列化为 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
  • 使用内容自适应:

确保表头视图内部的 UILabelUITextView 等控件启用了内容自适应,即 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: 是什么
URLfilepathstring 在 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 提供了 PublisherSubscriberOperator 等概念,可以方便地构建复杂的异步流。
  • 使用 RxSwift 框架

    • RxSwift 是一个 ReactiveX 的 Swift 实现。
    • RxSwift 也可以用于处理异步事件和数据流。
    • RxSwift 提供了 ObservableObserverOperator 等概念,与 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 提供了 zipmergecombineLatest 等操作符,可以用于整合多个 Publisher
  • 使用 RxSwift 框架

    • RxSwift 提供了 zipmergecombineLatest 等操作符,与 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 开发中的相关概念和技术。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容