Reputation: 1064
I'm trying to use SwiftUI's ObservableObject
, but I can't seem to update my view's properties.
This is my ContentView.swift
import SwiftUI
final class JiraData: ObservableObject {
init() {
// TODO use real service
self.worklog = self.jiraService.getCounter(tickets: 2, minutes: 120, status: "BELOW")
}
init(hours: Double, tickets: Int, status: Status) {
self.worklog = Worklog(minutesLogged: Int(hours) * 60, totalTickets: tickets, status: status)
}
/// Refreshes the data in this object, by calling the underlying service again
///
/// Despite SwiftUI's Observable pattern, I need the UI to toggle this interaction
func refresh() {
self.worklog = self.jiraService.getCounter(tickets: Int.random(in: 3..<5), minutes: 390, status: "OK")
print("Now mocked is: \(self.worklog)")
}
func getTimeAndTickets() -> String {
return "You logged \(String(format: "%.2f", Double(self.worklog.minutesLogged) / 60.0)) hours in \(self.worklog.totalTickets) tickets today"
}
func getStatus() -> String {
// TODO removed hardcoded part
return "You are on \"\(Status.below.rawValue)\" status"
}
var jiraService = MockedService()
@Published var worklog: Worklog
}
struct ContentView: View {
@ObservedObject var jiraData = JiraData()
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(jiraData.getTimeAndTickets())
.font(Font.system(size: 20.0))
.fontWeight(.semibold)
.multilineTextAlignment(.leading)
.padding(.horizontal, 16.0)
.frame(width: 360.0, height: 80.0, alignment: .topLeading)
Text(jiraData.getStatus())
.font(Font.system(size: 20.0))
.fontWeight(.semibold)
.multilineTextAlignment(.leading)
.padding(.horizontal, 16.0)
.frame(width: 360.0, height: 80, alignment: .topLeading)
Button(action: {
jiraData.refresh() // TODO unneeded?
})
{
Text("Refresh")
.font(.caption)
.fontWeight(.semibold)
}
Button(action: {
NSApplication.shared.terminate(self)
})
{
Text("Quit")
.font(.caption)
.fontWeight(.semibold)
}
.padding(.trailing, 16.0)
.frame(width: 360.0, alignment: .trailing)
}
.padding(0)
.frame(width: 360.0, height: 360.0, alignment: .top)
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(jiraData: JiraData(hours: 6.5, tickets: 2, status: Status.below))
}
}
#endif
The data structures are:
import Foundation
/// This is a Worklog possible status
enum Status: String, Codable, Equatable {
case below = "below"
case ok = "ok"
case overtime = "overtime"
public init(from decoder: Decoder) throws {
// If decoding fails, we default to "below"
guard let rawValue = try? decoder.singleValueContainer().decode(String.self) else {
self = .below
return
}
self = Status(rawValue: rawValue) ?? .below
}
}
/// This represents a Worklog on JIRA
struct Worklog: Codable {
var minutesLogged: Int
var totalTickets: Int
var status: Status
}
/// Parses a JSON obtained from the JAR's stdout
func parse(json: String) -> Worklog {
let decoder = JSONDecoder()
let data = Data(json.utf8)
if let jsonWorklogs = try? decoder.decode(Worklog.self, from: data) {
return jsonWorklogs
}
// TODO maybe handle error differently
return Worklog(minutesLogged: 0, totalTickets: 0, status: Status.below)
}
My Service is just a mock:
import Foundation
struct MockedService {
func getCounter(tickets: Int, minutes: Int, status: String) -> Worklog {
let json = "{\"totalTickets\":\(tickets),\"minutesLogged\":\(minutes),\"status\":\"\(status)\"}"
print("At MockedService: \(json)")
return parse(json: json)
}
}
On startup, I'm getting this printed on the console
At MockedService: {"totalTickets":2,"minutesLogged":120,"status":"BELOW"}
2020-11-09 20:47:17.163710-0300 JiraWorkflows[2171:14431] Metal API Validation Enabled
At this point, my app looks like this (which is correct).
I know, the UI looks awful so far :(
But then, after I click on Refresh
, the UI isn't updated, despite seeing this on the console
2020-11-09 20:47:17.163710-0300 JiraWorkflows[2171:14431] Metal API Validation Enabled
At MockedService: {"totalTickets":4,"minutesLogged":390,"status":"OK"}
Now mocked is: Worklog(minutesLogged: 390, totalTickets: 4, status: JiraWorkflows.Status.below)
Any ideas on what could be going on here?
Thanks in advance!
EDIT after @Asperi 's answer, my refresh()
function looks like this
func refresh() {
self.worklog = self.jiraService.getCounter(tickets: Int.random(in: 3..<5), minutes: 390, status: "OK")
print("Now mocked is: \(self.worklog)")
self.timeAndTicketsMsg = "You logged \(String(format: "%.2f", Double(self.worklog.minutesLogged) / 60.0)) hours in \(self.worklog.totalTickets) tickets today"
print(self.timeAndTicketsMsg)
self.statusMsg = "You are on \"\(self.worklog.status.rawValue)\" status"
print(statusMsg)
}
Which prints:
At MockedService: {"totalTickets":4,"minutesLogged":390,"status":"OK"}
Now mocked is: Worklog(minutesLogged: 390, totalTickets: 4, status: JiraWorkflows.Status.below)
You logged 6.50 hours in 4 tickets today
You are on "below" status
Which is correct. However, the UI isn't refreshed. I've also changed my ObservableObject
, which now looks like this:
final class JiraData: ObservableObject {
// rest of the class
var jiraService = MockedService()
var worklog: Worklog
@Published var timeAndTicketsMsg: String
@Published var statusMsg: String
}
and now, my ContentView
looks like this:
@ObservedObject var jiraData = JiraData()
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(jiraData.timeAndTicketsMsg)
// rest of the Text
Text(jiraData.statusMsg)
// rest of the class
}
Upvotes: 1
Views: 205
Reputation: 258365
The problem is in calling functions, like
Text(jiraData.getTimeAndTickets())
The refresh happens when there is dependency between changed property and view body. Function cannot be tracked for changes.
So you need to refactor your code to the kind
Text(jiraData.timeAndTickets)
where timeAndTickets
is
@Published var timeAndTickets: String
and you modify it directly whenever refresh
performed.... and then view will be updated correspondingly.
Upvotes: 2