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:表达式类型不明确,没有更多上下文,在视图之间传递 ObservableObject 时 [重复]