I am currently writing a ViewModel that uses the @Published property wrapper to update data pulled from by backend.
I have a class ViewResult that allows the frontend to display the state of the pulled data, so the published variable is of type ViewResult. I am worried my understanding of ObservableObject and @Published is wrong. Here is it's definition:
class ViewResult<T> : ObservableObject {
@Published var data: T?
var state: ViewResultState = .loading
var errorCode: APIResponseCode? = nil
init(){
self.state = .loading
self.data = nil
}
///OTHER COVIENIENCE INITIALIZERS NOT SHOWN
///Sets object and state variables if the APIResponse was successful.
///If it was unsuccessful, it sets error code and sets the ViewResult State to code.
func SetData(_ result: APIResult<T>) -> Void {
switch result {
case .success(let data):
self.data = data.data
self.state = .object
case .failure(let error):
self.errorCode = APIResponseCode.Error(error)
self.state = .code
}
}
The ViewModel itself is pretty basic, here is a specific example in which I am working:
class GuidelinesVM : ObservableObject {
var community_id: Int
@Published var guidelines: ViewResult<[Guideline]> = ViewResult(defaultValue: [])
private var dispatcher : ViewModelAPIDispatcher = ViewModelAPIDispatcher()
init(community_id: Int){
self.community_id = community_id
//Get guidelines before view is initialized.
//**NOTE: I AM CHANGING THIS TO EXECUTE IN THE ONAPPEAR CLOSURE
dispatcher.SendRequest(.getCommunityGuidlines(community_id: community_id)){ result in
self.guidelines.SetData(result)
}
}
func ReorderGuidelines(_ fromOffsets : IndexSet, _ toOffset : Int){
//Conversions from Swift's List system to mine
let oldOffset = fromOffsets.first! + 1
let guideline_id = self.guidelines.data![oldOffset - 1].id
let newOffset = toOffset > oldOffset ? toOffset : toOffset + 1
//Dispatch API Call
dispatcher.SendRequest(.reOrderGuidelines(community_id: community_id, guideline_id: guideline_id, fromOffset: oldOffset, toOffset: toOffset)) { apiResult in
if IsSuccessful(apiResult) {
self.guidelines.data!.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
}
func DeleteGuideline(_ offsets: IndexSet){
let index = offsets.first!
dispatcher.SendRequest(.deleteGuideline(community_id: community_id, guideline_id: index)){ result in
if IsSuccessful(result) {
self.guidelines.data!.remove(at: index)
}
}
}
func CreateGuidline(orderNumber: Int, text: String) {
dispatcher.SendRequest(.createGuideline(community_id: community_id, orderNumber: orderNumber, text: text)){
result in
if(IsSuccessful(result)){
//Right now just generate random int. I know this is terrible and lazy, but MVP
self.guidelines.data!.append(Guideline(id: Int.random(in: -1000...0), order_number: self.guidelines.data!.count + 1, text: text))
}
}
}
}
Now with that in mind, here is the view to which the @Published variable is being used:
struct CommunityGuidelinesView: View {
var psCommunityData : psCommunityClass
@State private var newGuideline : String = ""
@ObservedObject private var VM : GuidelinesVM
init(community : psCommunityClass){
self.psCommunityData = community
self.VM = GuidelinesVM(community_id: community.id)
}
var body: some View {
EditableView(userRole: psCommunityData.role){ inEditMode in
VStack{
APIResultView(apiResult: $VM.guidelines){ guidelines in
List {
// LIST DISPLAY CODE
}
.onDelete { indexSet in
VM.DeleteGuideline(indexSet)
}
.onMove{ fromOffsets, newOffset in
VM.ReorderGuidelines(fromOffsets, newOffset)
}
//NEED TO PASS A BINDING FOR NESTED VIEW CONTROL
if inEditMode.wrappedValue {
EditingView(
//VIEW IF NOT EDITING
},
EditingContent: {
//VIEW IF EDITING
}, completion: {
//FUNCTION EXECTUED ONCE DONE EDITING
VM.CreateGuidline(orderNumber: guidelines.count + 1, text: newGuideline)
}
)
}
}
}
}
}
}
Now with that knowledge, I believe the issue comes in with the editing view. I have the data displaying correctly when the view appears, and everyhere else in the application. I can successfully edit the object inside the class, I.E. adding to the list, deleting from it, and reording it. But the ListView that is displayedc, VM.Guidelines, doesn't update until "Edit Mode" is toggled back to "View Mode". The behavior goes something like this:
1.) Enter edit mode by hitting edit on the top right toolbar.
2.) Enter some text and execute VM.CreateGuideline().
3.) The API call is successful (I know this) and edits the data with the @Published guidelines variable.
4.) View doesn't update.
5.) Hit "Done" on the top right.
6.) List View is updated with new list item.
I'm worried that I am using these Combine defined aspects of Swift incorrectly. It also seems plausible, however that the nested nature of EditableView and APIResultView are getting in the way of one another in some capacity.
Also: One last piece of code that might be necessary is the APIResultView that handles the display logic for incoming data from the backend:
struct APIResultView<SuccessDisplay : View, T>: View {
@Binding var apiResult : ViewResult<T>
var content : (T) -> SuccessDisplay
var body: some View {
switch apiResult.state{
case .object:
content(apiResult.data!)
case .loading:
ProgressView("Loading...")
case .code:
if let errorCode = apiResult.errorCode {
HttpStatusView(statusCode: errorCode)
} else {
HttpStatusView(statusCode: apiResult.data as! APIResponseCode)
}
}
}
All things I've tried are just in hopes of getting the List to update.
I've tried adding @escaping closures to the VM function calls (CreateGuideline, DeleteGuideline, ReorderGuidelines). In hopes of editing the data from the View directly. I hate this because it defeats the point of my software architecture and the functions of objects in it. Luckily, it didn't work anyways.
I've tried passing the arguments through inout parameters in hopes that by making it a reference it would update one place in memory, though I think that happens anyways.
That's about it. Just been trial and erroring otherwise. No other thought out solutions.
While I didn't study your code or text in extreme detail, I noticed something that is probably causing your problem:
ViewResultis aclass, conformsObservableObject, and is also used as the type of aPublishedproperty inGuidelinesVM.This is a recipe for trouble.
The problem is that changes to the properties of a
ViewResult, including changes to thedataproperty and changes to properties of thedatavalue, will not show up as changes to the properties of theGuidelinesVM.For example:
I recommend changing
ViewResultto astruct(and removingObservableObjectand@Publishedfrom it). Making it astructwill makeGuidelinesVMsee any changes to its properties and fireobjectWillChange.If you can't make it a
structfor some reason, then you need to manually propagate changes inViewResultto changes inGuidelinesVM, perhaps by makingGuidelinesVMsubscribe toguidelines.objectWillChange: