簡體   English   中英

SwiftUI 自定義視圖的 ViewBuilder 不會在子類 ObservedObject 更新上重新渲染/更新

[英]SwiftUI custom View's ViewBuilder doesn't re-render/update on subclassed ObservedObject update

我已經研究了幾天,搜索了 Swift 和 SwiftUI 文檔、SO、論壇等,但似乎找不到答案。

這是問題所在;

我有一個 SwiftUI 自定義視圖,它對遠程資源的自定義 API 請求類進行一些狀態確定。 View 處理顯示加載狀態和失敗狀態,以及它的主體內容通過 ViewBuilder 傳遞,這樣如果來自 API 的狀態成功並且資源數據被加載,它將顯示頁面的內容。

問題是,當子類 ObservedObject 更新時,ViewBuilder 內容不會重新渲染。 對象更新以響應 UI(當按下按鈕時等),但 UI 永遠不會重新渲染/更新以反映子類 ObservedObject 中的更改,例如,子類 ObservedObject 中數組后面的 ForEach 在以下情況下不會刷新數組內容改變。 如果我將它移出自定義視圖,ForEach 將按預期工作。

我可以確認代碼編譯並運行。 觀察者和debugPrint()始終表明ApiObject正在正確更新狀態,並且視圖反映ApiState變化絕對正確。 它只是 ViewBuilder 的Content 我假設是因為 ViewBuilder 只會被調用一次。

編輯:上面的段落應該是提示, ApiState更新正確,但是在將大量日志記錄到應用程序后,UI 沒有監聽子類 ObservedObject 的發布。 屬性在變化,狀態也在變化,但 UI 並沒有對其做出反應。 另外,下一句被證明是錯誤的,我在 VStack 中再次測試,組件仍然沒有重新渲染,這意味着我找錯了地方!

如果是這種情況, VStack和其他此類元素如何解決這個問題? 還是因為我的ApiObjectView在狀態更改時被重新渲染,導致子視圖“重置”? 盡管在這種情況下,我希望它能夠接受新數據並按預期工作,但它永遠不會重新渲染。

有問題的代碼在下面的CustomDataList.swiftApiObjectView.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
            }
        )
    }
}

這是在將ObservedObject上的ForEach更改為其綁定數組屬性時出現問題的視圖。

// 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並且內容直接放在 View 中,則上述所有代碼都可以正常工作。

但是,這對於代碼重用和架構來說是可怕的,這種方式既漂亮又整潔,但不起作用。

有沒有其他方法可以解決這個問題,例如通過ViewModifierView擴展?

對此的任何幫助將不勝感激。

正如我所說,我似乎無法找到任何有此問題的人或任何在線資源,可以為我指明解決此問題的正確方向,或可能導致此問題的原因,例如 ViewBuilder 文檔中所述。

編輯:為了拋出一些有趣的東西,我已經向CustomDataList添加了一個倒數計時器,它每 1 秒更新一個標簽。 如果該計時器對象更新了文本,則重新渲染視圖,但當顯示倒計時時間的標簽上的文本更新時才會重新渲染。

把我的頭發拔了一個星期后才弄清楚,這是一個未記錄的問題,將ObservableObject子類化,如這個SO answer 所示

這尤其令人討厭,因為 Xcode 顯然會提示您刪除該類,因為父類提供了對ObservableObject繼承,所以在我看來一切都很好。

解決方法是,在子類中,通過相關@Published變量或任何您需要的willSet偵聽器手動觸發通用狀態更改self.objectWillChange.send()

在我提供的示例中,問題中的基類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聲明,而不會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聲明

它將正常工作並傳播狀態更改。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM