使用来自异步回调的数据推送视图
Push view with data from asynchronous callback
我正在尝试找到一种可行的方法来处理从异步回调返回的数据的导航。
考虑以下示例。 NavigationExampleView
中的按钮在单独的对象上触发一些异步方法,在本例中为 NavigationExampleViewModel
。从该方法返回的数据,然后应该 推送到 UserView
中的导航堆栈 上。 NavigationLink
似乎是存档的方法,但我找不到一种方法来获取我需要呈现的数据的非可选值。
struct User: Identifiable {
let id: String
let name: String
}
protocol API {
func getUser() -> AnyPublisher<User, Never>
}
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
NavigationLink.init(
destination: UserView(user: ???),
isActive: ???,
label: EmptyView.init
)
}
}
}
class NavigationExampleViewModel: ObservableObject {
@Published var isLoading = false
@Published var pushUser: User?
var cancellable: AnyCancellable?
let api: API
init(api: API) { self.api = api }
func getUser() {
isLoading = true
cancellable = api.getUser().sink { user in
self.pushUser = user
self.isLoading = false
}
}
}
struct UserView: View, Identifiable {
let id: String
let user: User
var body: some View {
Text(user.name)
}
}
问题:
- 如何获取数据以在视图中显示为非可选值?
- 我应该使用什么作为绑定来控制演示?
我几乎可以将其存档的一种方法是使用视图修饰符 .sheet(item: Binding<Identifiable?>, content: Identifiable -> View)
,如下所示:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
}.sheet(item: $vm.pushUser, content: UserView.init)
}
}
如何将相同的内容存档以将视图推送到导航堆栈,而不是将其呈现为 sheet?
您可以使用 if-let
来解包可选的 vm.pushUser
。然后,创建自定义绑定以将 Binding<User?>
映射到 Binding<Bool>
:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
if let user = vm.pushUser {
NavigationLink(
destination: UserView(id: "<id>", user: user),
isActive: binding,
label: EmptyView.init
)
}
}
}
var binding: Binding<Bool> {
.init(
get: { vm.pushUser != nil },
set: { if ![=10=] { vm.pushUser = nil } }
)
}
}
或者,使用它 内联:
if let user = vm.pushUser {
NavigationLink(
destination: UserView(id: "<id>", user: user),
isActive: .init(get: { vm.pushUser != nil }, set: { if ![=11=] { vm.pushUser = nil } }),
label: EmptyView.init
)
}
基于 @pawello2222 的回答中的 if let
方法,我创建了这个 ViewModifier
让你将视图推送到导航堆。它遵循与 .sheet
modifier.
相同的模式
import SwiftUI
private struct PushPresenter<Item: Identifiable, DestinationView: View>: ViewModifier {
let item: Binding<Item?>
let destination: (Item) -> DestinationView
let presentationBinding: Binding<Bool>
init(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> DestinationView) {
self.item = item
self.destination = content
presentationBinding = Binding<Bool> {
item.wrappedValue != nil
} set: { isActive in
if !isActive && item.wrappedValue != nil {
onDismiss?()
item.wrappedValue = nil
}
}
}
func body(content: Content) -> some View {
ZStack {
content
NavigationLink(
destination: item.wrappedValue == nil ? nil : destination(item.wrappedValue!),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}
extension View {
/// Pushed a View onto the navigation stack using the given item as a data source for the View's content. **Notice**: Make sure to use `.navigationViewStyle(StackNavigationViewStyle())` on the parent `NavigationView` otherwise using this modifier will cause a memory leak.
/// - Parameters:
/// - item: A binding to an optional source of truth for the view's presentation. When `item` is non-nil, the system passes the `item`’s content to the modifier’s closure. This uses a `NavigationLink` under the hood, so make sure to have a `NavigationView` as a parent in the view hierarchy.
/// - onDismiss: A closure to execute when poping the view of the navigation stack.
/// - content: A closure returning the content of the view.
func push<Item: Identifiable, Content: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content) -> some View
{
self.modifier(PushPresenter.init(item: item, onDismiss: onDismiss, content: content))
}
}
然后像这样使用它:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
Button("Get User") {
vm.getUser()
}
.push(item: $vm.pushUser, content: UserView.init)
}
}
请务必将您的实施视图保持在 NavigationView
中,以便 push
生效。
编辑
我发现此解决方案存在问题。如果使用引用类型作为绑定 item
,对象将不会在关闭时被取消初始化。似乎 content
持有对 item
的引用。一旦在绑定上设置了新对象(下次推送屏幕时),对象只会被取消初始化。
@pawello2222 的回答也是如此,但在使用 sheet
修饰符时不是问题。非常欢迎任何有关如何解决此问题的建议。
编辑 2
将 .navigationViewStyle(StackNavigationViewStyle())
添加到父 NavigationView
可以消除由于某种原因造成的内存泄漏。有关详细信息,请参阅答案 。
使用 if-let
方法导致推送视图转换停止工作。当数据不存在时,而是将 nil
传递给 NavigationLink
s destination
。我已经更新 ViewModifier
来处理这个问题。
我正在尝试找到一种可行的方法来处理从异步回调返回的数据的导航。
考虑以下示例。 NavigationExampleView
中的按钮在单独的对象上触发一些异步方法,在本例中为 NavigationExampleViewModel
。从该方法返回的数据,然后应该 推送到 UserView
中的导航堆栈 上。 NavigationLink
似乎是存档的方法,但我找不到一种方法来获取我需要呈现的数据的非可选值。
struct User: Identifiable {
let id: String
let name: String
}
protocol API {
func getUser() -> AnyPublisher<User, Never>
}
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
NavigationLink.init(
destination: UserView(user: ???),
isActive: ???,
label: EmptyView.init
)
}
}
}
class NavigationExampleViewModel: ObservableObject {
@Published var isLoading = false
@Published var pushUser: User?
var cancellable: AnyCancellable?
let api: API
init(api: API) { self.api = api }
func getUser() {
isLoading = true
cancellable = api.getUser().sink { user in
self.pushUser = user
self.isLoading = false
}
}
}
struct UserView: View, Identifiable {
let id: String
let user: User
var body: some View {
Text(user.name)
}
}
问题:
- 如何获取数据以在视图中显示为非可选值?
- 我应该使用什么作为绑定来控制演示?
我几乎可以将其存档的一种方法是使用视图修饰符 .sheet(item: Binding<Identifiable?>, content: Identifiable -> View)
,如下所示:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
}.sheet(item: $vm.pushUser, content: UserView.init)
}
}
如何将相同的内容存档以将视图推送到导航堆栈,而不是将其呈现为 sheet?
您可以使用 if-let
来解包可选的 vm.pushUser
。然后,创建自定义绑定以将 Binding<User?>
映射到 Binding<Bool>
:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
HStack {
Button("Get User") {
vm.getUser()
}
if let user = vm.pushUser {
NavigationLink(
destination: UserView(id: "<id>", user: user),
isActive: binding,
label: EmptyView.init
)
}
}
}
var binding: Binding<Bool> {
.init(
get: { vm.pushUser != nil },
set: { if ![=10=] { vm.pushUser = nil } }
)
}
}
或者,使用它 内联:
if let user = vm.pushUser {
NavigationLink(
destination: UserView(id: "<id>", user: user),
isActive: .init(get: { vm.pushUser != nil }, set: { if ![=11=] { vm.pushUser = nil } }),
label: EmptyView.init
)
}
基于 @pawello2222 的回答中的 if let
方法,我创建了这个 ViewModifier
让你将视图推送到导航堆。它遵循与 .sheet
modifier.
import SwiftUI
private struct PushPresenter<Item: Identifiable, DestinationView: View>: ViewModifier {
let item: Binding<Item?>
let destination: (Item) -> DestinationView
let presentationBinding: Binding<Bool>
init(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> DestinationView) {
self.item = item
self.destination = content
presentationBinding = Binding<Bool> {
item.wrappedValue != nil
} set: { isActive in
if !isActive && item.wrappedValue != nil {
onDismiss?()
item.wrappedValue = nil
}
}
}
func body(content: Content) -> some View {
ZStack {
content
NavigationLink(
destination: item.wrappedValue == nil ? nil : destination(item.wrappedValue!),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}
extension View {
/// Pushed a View onto the navigation stack using the given item as a data source for the View's content. **Notice**: Make sure to use `.navigationViewStyle(StackNavigationViewStyle())` on the parent `NavigationView` otherwise using this modifier will cause a memory leak.
/// - Parameters:
/// - item: A binding to an optional source of truth for the view's presentation. When `item` is non-nil, the system passes the `item`’s content to the modifier’s closure. This uses a `NavigationLink` under the hood, so make sure to have a `NavigationView` as a parent in the view hierarchy.
/// - onDismiss: A closure to execute when poping the view of the navigation stack.
/// - content: A closure returning the content of the view.
func push<Item: Identifiable, Content: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content) -> some View
{
self.modifier(PushPresenter.init(item: item, onDismiss: onDismiss, content: content))
}
}
然后像这样使用它:
struct NavigationExampleView: View {
@ObservedObject var vm: NavigationExampleViewModel
var body: some View {
Button("Get User") {
vm.getUser()
}
.push(item: $vm.pushUser, content: UserView.init)
}
}
请务必将您的实施视图保持在 NavigationView
中,以便 push
生效。
编辑
我发现此解决方案存在问题。如果使用引用类型作为绑定 item
,对象将不会在关闭时被取消初始化。似乎 content
持有对 item
的引用。一旦在绑定上设置了新对象(下次推送屏幕时),对象只会被取消初始化。
@pawello2222 的回答也是如此,但在使用 sheet
修饰符时不是问题。非常欢迎任何有关如何解决此问题的建议。
编辑 2
将 .navigationViewStyle(StackNavigationViewStyle())
添加到父 NavigationView
可以消除由于某种原因造成的内存泄漏。有关详细信息,请参阅答案
使用 if-let
方法导致推送视图转换停止工作。当数据不存在时,而是将 nil
传递给 NavigationLink
s destination
。我已经更新 ViewModifier
来处理这个问题。