【问题标题】:SwiftUI/iOS 15: fullScreenCover not dismissing with binding on published varSwiftUI/iOS 15:fullScreenCover 不会通过绑定已发布的 var 来解除
【发布时间】:2021-12-20 22:59:53
【问题描述】:

我的应用程序使用 SwiftUI,以 iOS 15+ 为目标,并由 ObservableObjectAppState 管理,作为整个应用程序的真实来源。我使用@Published vars 来管理各个地方的覆盖状态(例如,加载数据时)没有问题。

简体AppState

class AppState:ObservableObject {
  @Published var showLoadingOverlay = false

  func loadData() {
    self.showLoadingOverlay = true
    // fech data
    self.showLoadingOverlay = false
  }
}

在视图中,我通过@EnvironmentObject 注入AppState 并使用发布的变量来控制fullScreenCover

简化示例:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    ScrollView {
      VStack {
        ForEach(Array(zip(state.dataSets.indices, state.dataSets)), id: \.0) { (dataSetIndex, dataSet) in
          MirrorChartView(dataSet: dataSet)
        }
      }
    }
    .onAppear {
      state.loadData()
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

问题我遇到的是在绑定值更改为false 时仍然显示一个叠加层。这种“卡住”仅发生在loadData() 完成速度非常快并且覆盖在完全显示之前实际上会被关闭的情况。

在视图中添加调试监视器确认与AppState 的绑定已正确传播:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { showOverlay in
      print("show overlay: \(showOverlay)")
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

即当覆盖层向上滑动时,我可以看到日志show overlay: false,但它不会被关闭。

即使通过 @State 本地绑定添加间接也不能可靠地解决问题:

struct MyDataView:View {
  @EnvironmentObject var state:AppState
  @State var showLoadingOverlay = false

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { showOverlay in
      self.showLoadingOverlay = showOverlay
    }
    .fullScreenCover(isPresented: self.$showLoadingOverlay) {
      LoadingOverlay()
    }
  }
}

更新:更多测试表明,即使就地绑定为 false 也不会关闭 fullScreenCover

将状态绑定替换为就地自定义绑定显示我的状态已正确传播并导致视图更新。但是,工作表仍然存在。

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    VStack {
      // draw stuff
    }
    .onAppear {
      state.loadData()
    }
    .fullScreenCover(isPresented: .init(
      get: {
        print("recomputing binding: \(state.showOverlay)")
        return state.showOverlay
      }, set: { newValue in
        state.showOverlay = newValue
      }
    )) {
      LoadingOverlay()
    }
  }
}

这导致以下日志消息年表:

recomputing binding: false
recomputing binding: true
recomputing binding: false

更新:进一步的发现

我不知道该怎么做,但是在LoadingOverlay.onAppear() 中添加一条日志消息显示,它出现在之后绑定已更改为false:

struct MyDataView:View {
  @EnvironmentObject var state:AppState

  var body: some View {
    ScrollView {
      VStack {
        ForEach(Array(zip(state.dataSets.indices, state.dataSets)), id: \.0) { (dataSetIndex, dataSet) in
          MirrorChartView(dataSet: dataSet)
        }
      }
    }
    .onAppear {
      state.loadData()
    }
    .onChange(of: state.showLoadingOverlay) { show in
      print("show loading overlay \(show)")
    }
    .fullScreenCover(isPresented: $state.showLoadingOverlay) {
      LoadingOverlay()
        .onAppear {
          print("loading overlay appears")
        }
    }
  }
}

这导致以下日志消息年表:

show loading overlay: true
show loading overlay: false
loading overlay appears

有什么方法可以解决这个问题?

【问题讨论】:

  • 我无法使用 code like this 复制它。你能举一个可重现的例子吗?
  • 你能分享一下fullscreenCover是什么吗?
  • @SteveM 您的示例几乎就是正在发生的事情以及为什么我对这种行为如此困惑。在我的例子中,loadData() 使用 combineLatest 聚合来自两个源的数据并更新 state 中的另一个 @Published var。由于我在整个应用程序的许多地方都使用了这种模式,因此我很难创建一个可重现的示例,即它适用于其他任何地方。但是,其他地方实际上调用 API 并且速度相对较慢,而在这种情况下,它只是使用发布者管道进行一些过滤和聚合,如果该数据已经加载,则基本上会立即发生。
  • @AvnerBarr .fullScreenCover() 中的代码只显示了一个 dump 加载覆盖,即只是一个带有一些文本的视图和一个 ProgressView。加载覆盖本身既没有@State,也没有访问AppState。纯渲染。
  • @SteveM 我刚刚意识到您的示例与我正在执行的操作有所不同:加载是由onAppear() 触发的,而不是通过单击按钮触发的。不过这应该没什么区别。

标签: ios swift swiftui combine


【解决方案1】:

尝试在LoadingOverlayonAppear() 中加载数据。这样一来,您就可以延迟加载,并且可以防止导致问题的任何条件:

import SwiftUI

class AppState: ObservableObject {
    @Published var showLoadingOverlay = false
    
    func loadData() {
        //load data
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.showLoadingOverlay = false
        }
    }
}

struct LoadingOverlay: View {
    @EnvironmentObject var state: AppState
    var body: some View {
        VStack {
            Text("Loading")
        }
        .onAppear {
            state.loadData()
        }
    }
}

struct ContentView: View {
    @StateObject var state = AppState()
    var body: some View {
        VStack {
            Text("Data View")
        }
        .onAppear {
            state.showLoadingOverlay = true
        }
        
        .fullScreenCover(isPresented: $state.showLoadingOverlay) {
            LoadingOverlay()
        }
        .environmentObject(state)
    }
}

【讨论】:

  • 嗯...这可能值得一试,但不幸的是需要对我的状态逻辑进行重大重新布线。目前它的实现方式是:状态决定 UI,即loadData() 调用导致发布的showLoadingOverlay 变量由于AppState 中的幕后逻辑而变为真/假。这控制是否显示覆盖。我当前的应用程序逻辑无法轻易更改,以便用户可以直接导航到叠加层,然后加载数据。
  • 我现在已经尝试了一百万件事 - 可靠地让覆盖为我解除的唯一方法是您建议的变体:我创建了一个以 true 开头的 @State var showOverlay在视图中。我按照您的建议将loadData() 调用移动到覆盖的onAppear,并添加对state.showLoadingOverlay 的订阅,将其显式连接到onAppear() 中的视图showOverlay 状态变量,即$state.showLoadingOverlay.sink { loadingState in self.showOverlay = loadingState }.store(...)。我仍然对此感到困惑。
  • @haensl 我在 SwiftUI 中看到了一些与此类似的东西,发布者快速发布。我不会担心它,找到解决方法并继续前进。如果您可以向 Apple 发送反馈。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-13
  • 1970-01-01
  • 1970-01-01
  • 2020-08-11
  • 1970-01-01
  • 2022-01-04
  • 1970-01-01
相关资源
最近更新 更多