SwiftUI 为啥不能在视图之间传递发布者?

Posted

技术标签:

【中文标题】SwiftUI 为啥不能在视图之间传递发布者?【英文标题】:SwiftUI Why Can't pass a publisher between Views?SwiftUI 为什么不能在视图之间传递发布者? 【发布时间】:2020-11-11 03:17:36 【问题描述】:

我只是想做一些这样的测试↓

    从第一个视图创建一个发布者 将其传递给第二个视图 在第二个视图中将发布者与某些属性绑定并尝试在屏幕上显示它

代码是↓(第一个视图)

struct ContentView: View 
    
    let publisher = URLSession(configuration: URLSessionConfiguration.default)
        .dataTaskPublisher(for: URLRequest(url: URL(string: "https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8")!))
        .map(\.data.description)
        .replaceError(with: "Error!")
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
        
    
    var body: some View 
        NavigationView 
            List 
                NavigationLink(destination: ResponseView(publisher: publisher)) 
                    Text("Hello, World!")
                
            .navigationBarTitle("Title", displayMode: .inline)
        
    

(第二视图)

struct ResponseView: View 
    
    let publisher: AnyPublisher<String, Never>
    @State var content: String = ""
    
    var body: some View 
        HStack 
            VStack 
                Text(content)
                    .font(.system(size: 12))
                    .onAppear  _ = self.publisher.assign(to: \.content, on: self) 
                Spacer()
            
            Spacer()
        
        
    

但代码不起作用。请求失败并显示消息 ↓

2020-11-11 11:08:04.657375+0800 PandaServiceDemo[83721:1275181] Task <6B53516E-5127-4C5E-AD5F-893F1AEE77E8>.<1> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo=NSErrorFailingURLStringKey=https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8, NSLocalizedDescription=cancelled, NSErrorFailingURLKey=https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8

有人能告诉我发生了什么吗?正确的方法是什么?

【问题讨论】:

***.com/a/63239902/341994 【参考方案1】:

这里的问题是,订阅没有存储在任何地方。您必须将其存储在 AnyCancellable 变量中并保留订阅。

在调试组合相关问题时使用.print() 运算符。我觉得它真的很有用。


正确的做法是将发布者和订阅提取到ObservableObject 并注入到View 或使用@StateObject

class DataProvider: ObservableObject 
    @Published var content: String = ""
    private var bag = Set<AnyCancellable>()
    private let publisher: AnyPublisher<String, Never>
    init() 
        publisher = URLSession(configuration: URLSessionConfiguration.default)
            .dataTaskPublisher(for: URLRequest(url: URL(string: "https://v.juhe.cn/joke/content/list.php?sort=asc&page=&pagesize=&time=1418816972&key=aa73ebdd8672a2b9adc9dbb2923184c8")!))
            .map(\.data.description)
            .print()
            .replaceError(with: "Error!")
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    
    
    func loadData() 
        publisher.assign(to: \.content, on: self).store(in: &bag)
    


struct ContentView: View 
    @StateObject var dataProvider = DataProvider()
    
    var body: some View 
        NavigationView 
            List 
                NavigationLink(destination: ResponseView(dataProvider: dataProvider)) 
                    Text("Hello, World!")
                
            .navigationBarTitle("Title", displayMode: .inline)
        
    


struct ResponseView: View 
    let dataProvider: DataProvider
    
    var body: some View 
        HStack 
            VStack 
                Text(dataProvider.content)
                    .font(.system(size: 12))
                    .onAppear 
                        self.dataProvider.loadData()
                    
                Spacer()
            
            Spacer()
        
        
    

请注意,我们使用@StateObject 来确保DataProvider 实例在视图更新时不会被破坏。

【讨论】:

在您的示例中,您不妨将@StateObject 放在 ResponseView 中【参考方案2】:

订阅需要保存,否则会被反初始化并自动取消。

通常是这样完成的:

var cancellables: Set<AnyCancellable> = []
// ...
publisher
  .sink ...
  .store(in: &cancellables)

所以,你可以像上面一样创建@State属性,也可以使用.onReceive

let publisher: AnyPublisher<String, Never>

var body: some View 
   HStack 
     // ...
   
   .onReceive(publisher) 
      content = $0
   


您应该小心使用上述方法,因为如果 ResponseView 被重新初始化,它将获得发布者的副本(大多数发布者都是值类型),所以它会开始一个新的请求。

为避免这种情况,请将.share() 添加到发布者:

let publisher = URLSession(configuration: URLSessionConfiguration.default)
        .dataTaskPublisher(...)
        //...
        .share()
        .eraseToAnyPublisher()

【讨论】:

【参考方案3】:

就 SwiftUI 而言,您在做一些根本错误的事情:从视图创建发布者。这意味着 每次 实例化 ContentView 时都会创建一个新的发布者,并且出于各种方式和目的,这可能会发生很多次,SwiftUI 不保证 View 只会实例化一次。

您需要做的是将发布的内容提取到某个对象中,该对象要么从上游注入,要么由 SwiftUI 通过@StateObject 管理。

【讨论】:

谢谢,这对我很有帮助。顺便一提。由于@StateObject 仅适用于 ios14。如果在 iOS 13 中我们可以做什么? @Shaggon 我能想到的一种方法是在顶层创建一个大模型,然后注入下游。这样,您只有一个实例,并且在每个级别上,您只能发送该大模型所需的部分。唯一(大)的缺点是模型可以通过这种方法变大......【参考方案4】:

这个工作有两种方法:一种更好

方式一:

    import SwiftUI




struct ContentView: View 
    
    @State var urlForPublicsh: URL?
    
    
    var body: some View 
        
        VStack
        
            Text(urlForPublicsh?.absoluteString ?? "nil")
                .padding()
            
            Button("Change the Publisher") urlForPublicsh = URL(string: "https://***.com")
                .padding()
            
            
            SecondView(urlForPublicsh: $urlForPublicsh)
            
        
        .onAppear()
        
            urlForPublicsh = URL(string: "https://www.apple.com")
        
  
    

struct SecondView: View 
    @Binding var urlForPublicsh: URL?
    
    var body: some View 
        
        Text(urlForPublicsh?.absoluteString ?? "nil")
            .padding()
    

方式 2:

    import SwiftUI

class UrlForPublicshModel: ObservableObject

    static let shared = UrlForPublicshModel()
    @Published var url: URL?




struct ContentView: View 
    

    @StateObject var urlForPublicshModel = UrlForPublicshModel.shared
    
    var body: some View 
        
        VStack
        
            Text(urlForPublicshModel.url?.absoluteString ?? "nil")
                .padding()
            
            Button("Change the Publisher") urlForPublicshModel.url = URL(string: "https://***.com")
                .padding()
            
            
            SecondView()
            
        
        .onAppear()
        
            urlForPublicshModel.url = URL(string: "https://www.apple.com")
        
  
    



struct SecondView: View 

    @ObservedObject var urlForPublicshModel = UrlForPublicshModel.shared
    
    var body: some View 
        
        Text(urlForPublicshModel.url?.absoluteString ?? "nil")
            .padding()

    

【讨论】:

以上是关于SwiftUI 为啥不能在视图之间传递发布者?的主要内容,如果未能解决你的问题,请参考以下文章

在 SwiftUi 中的视图之间传递列表

如何在swiftui中的视图之间传递变量?

如何在swiftui中的视图之间传递数据?

SwiftUI:表达式类型不明确,没有更多上下文,在视图之间传递 ObservableObject 时 [重复]

为啥 SwiftUI ObservedObject 不能在列表行中工作?

在 2 个页面 SwiftUI 之间传递状态