Я изучал это несколько дней, изучая документы Swift & SwiftUI, SO, форумы и т. Д. c. и не могу найти ответ.
Вот проблема;
У меня есть пользовательское представление SwiftUI, которое выполняет определенное состояние для пользовательского класса запроса API для удаленного ресурса. View обрабатывает состояния загрузки и состояния сбоя, а также содержимое его тела, передаваемое через ViewBuilder, так что если состояние из API является успешным и данные ресурса загружаются, он отображает содержимое страницы.
Проблема в том, что содержимое ViewBuilder не отображается повторно при обновлении подкласса ObservedObject. Объект обновляется в ответ на пользовательский интерфейс (при нажатии кнопок и т. Д. c.), Но пользовательский интерфейс никогда не выполняет повторную визуализацию / обновление, чтобы отразить изменение в подклассе ObservedObject, например, ForEach за массивом в подклассе ObservedObject делает не refre sh при изменении содержимого массива. Если я переместу его из пользовательского представления, ForEach будет работать так, как задумано.
Я могу подтвердить, что код компилируется и запускается. Наблюдатели и повсюду debugPrint()
показывают, что ApiObject
корректно обновляет состояние, а представление полностью отражает изменение ApiState
. Это просто Content
ViewBuilder. Я полагаю, это потому, что ViewBuilder будет вызываться только один раз.
EDIT : вышеупомянутый абзац должен был быть подсказкой, ApiState
обновляется правильно, но после интенсивной регистрации в приложении пользовательский интерфейс не прослушивал публикацию подкласса ObservedObject. Свойства менялись, и состояние тоже было, но пользовательский интерфейс не реагировал на это. Кроме того, следующее предложение оказалось ложным, я снова проверил в VStack, и компонент все еще не рендерился, то есть я искал не в том месте!
Если это так, как VStack
и другие подобные элементы обходят это? Или это потому, что мой ApiObjectView
перерисовывается при изменении состояния, в результате чего дочернее представление «сбрасывается»? Несмотря на то, что в этих обстоятельствах я ожидал бы, что он примет новые данные и будет работать так, как ожидалось, в любом случае, он просто никогда не будет повторно отображаться.
Код проблемного c находится в 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 секунду. ЕСЛИ текст обновляется этим объектом таймера, представление перерисовывается, но ТОЛЬКО , когда текст на метке, отображающей время обратного отсчета, обновляется.