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
}
