我已经研究了这一天,搜寻了Swift和SwiftUI文档,SO,论坛等,但似乎找不到答案。
这是问题所在
我有一个SwiftUI自定义视图,该视图对到远程资源的自定义API请求类进行一些状态确定。 View会显示加载状态和失败状态,并通过ViewBuilder传递其主体内容,这样,如果API中的状态成功并且资源数据已加载,它将显示页面的内容。
问题是,子类ObservedObject更新时,ViewBuilder内容不会重新呈现。对象会根据UI进行更新(按下按钮等时),但UI不会重新渲染/更新以反映子类ObservedObject内的更改,例如,子类ObservedObject内的数组后面的ForEach在以下情况下不会刷新数组内容更改。如果将其移出自定义视图,则ForEach会按预期工作。
我可以确认代码已编译并运行。观察者和debugPrint()
的全部显示ApiObject
正在正确更新状态,并且View反映了ApiState
的更改绝对正确。它只是ViewBuilder的Content
。我认为这是因为ViewBuilder只会被调用一次。
编辑:上面的段应该是提示,ApiState
已正确更新,但是在将大量日志记录到应用程序中之后,UI并未监听子类的发布。 ObservedObject。属性在变化,状态也在变化,但是UI对此没有反应。
此外,下句话证明是错误的,我在VStack中再次进行了测试,但该组件仍然没有重新渲染,这意味着我在错误的位置查找了
如果是这种情况,VStack
和其他此类元素如何解决呢?
还是因为状态更改导致我的ApiObjectView
被重新渲染而导致子视图“重置”?尽管在这种情况下,我希望它可以接管新数据并按预期工作,但它永远不会重新呈现。
有问题的代码在下面的CustomDataList.swift
和ApiObjectView.swift
中。我留下评论指向正确的方向。
这是示例代码;
// ApiState.swift
// Stores the API state for where the request and data parse is currently at.
// This drives the ApiObjectView state UI.
import Foundation
enum ApiState: String
{
case isIdle
case isFetchingData
case hasFailedToFetchData
case isLoadingData
case hasFailedToLoadData
case hasUsableData
}
// ApiObject.swift
// A base class that the Controllers for the app extend from.
// These classes can make data requests to the remote resource API over the
// network to feed their internal data stores.
class ApiObject: ObservableObject
{
@Published var apiState: ApiState = .isIdle
let networkRequest: NetworkRequest = NetworkRequest(baseUrl: "https://api.example.com/api")
public func apiGetJson<T: Codable>(to: String, decodeAs: T.Type, onDecode: @escaping (_ unwrappedJson: T) -> Void) -> Void
{
self.apiState = .isFetchingData
self.networkRequest.send(
to: to,
onComplete: {
self.apiState = .isLoadingData
let json = self.networkRequest.decodeJsonFromResponse(decodeAs: decodeAs)
guard let unwrappedJson = json else {
self.apiState = .hasFailedToLoadData
return
}
onDecode(unwrappedJson)
self.apiState = .hasUsableData
},
onFail: {
self.apiState = .hasFailedToFetchData
}
)
}
}
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
// Subclassed from the ApiObject, inheriting ObservableObject
import Foundation
import Combine
class CustomDataController: ApiObject
{
@Published public var customData: [CustomDataStruct] = []
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
这是在将ForEach
更改为其绑定数组属性时重新呈现其ObservedObject
的视图。
// CustomDataList.swift
// This is the SwiftUI View that drives the content to the user as a list
// that displays the CustomDataController.customData.
// The ForEach in this View
import SwiftUI
struct CustomDataList: View
{
@ObservedObject var customDataController: CustomDataController = CustomDataController()
var body: some View
{
ApiObjectView(
apiObject: self.customDataController,
onQuit: {}
) {
List
{
Section(header: Text("Custom Data").padding(.top, 40))
{
ForEach(self.customDataController.customData, id: \.self, content: { customData in
// This is the example that doesn't re-render when the
// customDataController updates its data. I have
// verified via printing at watching properties
// that the object is updating and pushing the
// change.
// The ObservableObject updates the array, but this ForEach
// is not run again when the data is changed.
// In the production code, there are buttons in here that
// change the array data held within customDataController.customData.
// When tapped, they update the array and the ForEach, when placed
// in the body directly does reflect the change when
// customDataController.customData updates.
// However, when inside the ApiObjectView, as by this example,
// it does not.
Text(customData.textProperty)
})
}
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Learn"))
.onAppear() {
self.customDataController.fetch()
}
}
}
struct CustomDataList_Previews: PreviewProvider
{
static var previews: some View
{
CustomDataList()
}
}
这是有问题的自定义视图,不会重新呈现其内容。
// ApiObjectView
// This is the containing View that is designed to assist in the UI rendering of ApiObjects
// by handling the state automatically and only showing the ViewBuilder contents when
// the state is such that the data is loaded and ready, in a non errornous, ready state.
// The ViewBuilder contents loads fine when the view is rendered or the state changes,
// but the Content is never re-rendered if it changes.
// The state renders fine and is reactive to the object, the apiObjectContent
// however, is not.
import SwiftUI
struct ApiObjectView<Content: View>: View {
@ObservedObject var apiObject: ApiObject
let onQuit: () -> Void
let apiObjectContent: () -> Content
@inlinable public init(apiObject: ApiObject, onQuit: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
self.apiObject = apiObject
self.onQuit = onQuit
self.apiObjectContent = content
}
func determineViewBody() -> AnyView
{
switch (self.apiObject.apiState) {
case .isIdle:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isFetchingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isLoadingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .hasFailedToFetchData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasFailedToLoadData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasUsableData:
return AnyView(
VStack
{
self.apiObjectContent()
}
)
}
}
var body: some View
{
self.determineViewBody()
}
}
struct ApiObjectView_Previews: PreviewProvider {
static var previews: some View {
ApiObjectView(
apiObject: ApiObject(),
onQuit: {
print("I quit.")
}
) {
EmptyView()
}
}
}
现在,如果不使用ApiObjectView
并将其内容直接放置在视图中,则上述所有代码都可以正常工作。
但是,这对于代码重用和体系结构来说太可怕了,这样虽然好看又整洁,但是不起作用。
还有其他方法可以解决此问题吗,例如是通过ViewModifier
还是View
扩展名?
对此将提供任何帮助。
正如我所说,我似乎找不到任何人遇到此问题,也找不到任何在线资源可以为我指出正确的方向来解决此问题,或者可能是由什么引起的,例如ViewBuilder文档中概述的内容。
编辑:为了添加一些有趣的东西,自那以后,我在CustomDataList
中添加了倒数计时器,该计时器每1秒更新一次标签。 如果由该计时器对象更新文本,则重新渲染视图,但仅(当标签上显示倒计时时间的文本更新时)。
答案 0 :(得分:5)
拔出头发一周后就发现了它,这是ObservableObject
子类化的一个未记录的问题,如本SO answer所示。
这特别令人讨厌,因为Xcode显然提示您删除该类,因为父类提供了对ObservableObject
的继承,所以在我看来一切都很好。
此修复程序是在子类中,用于通过有问题的self.objectWillChange.send()
或任何您需要的变量上的willSet
侦听器手动触发通用状态更改@Published
。
在我提供的示例中,问题中的基类ApiObject
保持不变。
尽管如此,CustomDataController
需要进行如下修改:
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
@Published public var customData: [CustomDataStruct] = [] {
willSet {
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
我添加了手动发布后,问题就解决了。
链接答案中的重要提示:请勿在子类上重新声明objectWillChange
,因为这将再次导致状态无法正确更新。例如。声明默认值
let objectWillChange = PassthroughSubject<Void, Never>()
子类上的会再次中断状态更新,这需要保留在直接从ObservableObject
扩展的父类上,无论是我的手动还是自动默认定义(键入或不键入,并保留为继承的声明)
尽管您仍然可以根据需要定义任意数量的自定义PassthroughSubject
声明,例如在子类上没有问题
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
var customDataWillUpdate = PassthroughSubject<[CustomDataStruct], Never>()
@Published public var customData: [CustomDataStruct] = [] {
willSet {
// Custom state change handler.
self.customDataWillUpdate.send(newValue)
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
只要
self.objectWillChange.send()
保留在子类上所需的@Published
属性上PassthroughSubject
声明它将工作并正确传播状态更改。