Search code examples
swiftuiintervalsswiftui-list

SwiftUI isn't loading data


Hi I'm building on an application that gets events from json file sample like this:

[{"title": "Evening Picnic", "start": "November 10, 2018 6:00 PM", "end": "November 10, 2018 7:00 PM"}]

and after it parse the events it has to sort them in order by date and group them by date as well in List Section also checks for conflicts between events and changes the row color to red for example.

I made a simple UI, a list with dynamic sections and the events sorted inside, but for some reason the app compiles with no errors or crashes but the view is blank, like the data weren't loaded to the view.

Here is my code so far:

Events.swift

public struct Event: Codable, IntervalProtocol, Hashable, Identifiable {
    public var id = UUID()
    var title: String
    var start: String
    var end: String
}

struct AppConstants {
    static let mockDataFilename = "mock"
    static let jsonDataFormat = "MMMM d, yyyy h:mm a"
    static let sectionDateFormat = "MMMM d, yyyy"
    static let eventDateFormat = "h:mm a"
}

extension Event {
    public static var jsonDateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = AppConstants.jsonDataFormat
        return formatter
    }()
    // Parse dates into Struct variables.
    var startDate: Date {
        guard let date = Event.jsonDateFormatter.date(from: self.start) else {
            fatalError("Unable to parse date.")
        }
        return date
    }

    var endDate: Date {
        guard let date = Event.jsonDateFormatter.date(from: self.end) else {
            fatalError("Unable to parse date.")
        }
        return date
    }

    var interval: Interval {
        return Interval(self.startDate.timeIntervalSince1970, self.endDate.timeIntervalSince1970)
    }
}

ViewModel.swift

class ViewModel: ObservableObject {
    let queue = DispatchQueue(label: "com.elbeheiry")

    @Published var sections = [Date: [Event]]()
    @Published var sortedDays = [Date]()
    @Published var intervalTree: IntervalTree!

    func getEvents() {
        let events = Bundle.main.eventJsonData(fileName: AppConstants.mockDataFilename)
        self.intervalTree = IntervalTree(events ?? [])
        let inOrder = self.intervalTree.inOrder.map { $0 as! Event }
        self.buildSections(inOrder)
    }

    func buildSections(_ events: [Event]) {
        for event in events {
            let startTimestamp = Date(timeIntervalSince1970: event.interval.min)
            let strippedDate = Calendar.current.dateComponents([.year, .month, .day],
                                                               from: startTimestamp)

            guard let date = Calendar.current.date(from: strippedDate) else {
                fatalError("Failed to remove time from Date object")
            }
            self.sections[date, default: []].append(event)
        }
        self.sortedDays = self.sections.keys.sorted()
    }
}

public extension Bundle {
    func eventJsonData(fileName: String) -> [Event]? {
        guard let url = self.url(forResource: fileName, withExtension: "json") else {
            fatalError("File was not reachable.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("File was not readable.")
        }

        return try? JSONDecoder().decode([Event].self, from: data)
    }
}

ContentView.swift

struct ContentView: View {
    @ObservedObject var viewModel: ViewModel = ViewModel()

    var body: some View {
        VStack {
            List {
                ForEach(viewModel.sortedDays, id: \.self) { header in
                    Section(header: Text(header, style: .date)) {
                        ForEach(viewModel.sections[header]!) { event in
                            EventCell(event: event)
                                .background(Color(self.viewModel.intervalTree.isConflicted(event) ? #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1) : #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)))
                        }
                    }
                }
            }
        }
        .onAppear(perform: {
            viewModel.getEvents()
        })
    }
}

Solution

  • It's because you are using Codable, and it is trying to decode id from the JSON. The id isn't in your JSON, and I can see you want each new instance to create its own ID.

    If you printed an error in a catch statement, by using do-try-catch rather than try?, you would have seen the following:

    keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: "id", intValue: nil) ("id").", underlyingError: nil))

    Basically, they is no value associated with the key id, because the key is not in the JSON.

    You need to create custom CodingKeys to exclude id from being encoded/decoded:

    public struct Event: Codable, Hashable, Identifiable {
        private enum CodingKeys: CodingKey {
            case title
            case start
            case end
        }
    
        public var id = UUID()
        var title: String
        var start: String
        var end: String
    }