I have a problem with fetching data from API. The DayViewCalendar
is creating View
before events data is fetched from API.
My main view is in SwiftUI
struct CalendarScreen: View {
@StateObject private var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
ZStack(alignment: .trailing) {
CalendarKitDisplayView(viewModel: viewModel)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
I have a ViewModel which is fetching events data from API
import Combine
import Foundation
extension NSNotification.Name {
static let onEventLoaded = Notification.Name("onEventLoaded")
}
extension CalendarScreen {
class ViewModel: ObservableObject {
let calendarService = CalendarService()
@Published var calendarEvents: [CalendarEvent]
var cancellable: AnyCancellable?
init() {
self.calendarEvents = [CalendarEvent()]
}
func fetchCalendarEvents() {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .onEventLoaded, object: nil)
})
}
}
}
Calendar Service is just a service for singletion of repository
import Foundation
import Combine
struct CalendarService {
private var calendarRepository = CalendarRepository()
func getEvents() -> AnyPublisher<[CalendarEvent], Error> {
return calendarRepository.getEvents()
}
}
And calendarRepository is just simple URL Request for my API
import Combine
struct CalendarRepository {
private let agent = Agent()
private let calendarurl = "\(api)/calendars_events"
func getEvents() -> AnyPublisher<[CalendarEvent], Error>{
let urlString = "\(calendarurl)"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
return agent.run(request)
}
}
Agent is handling the request
class Agent {
let session = URLSession.shared
var cancelBag: Set<AnyCancellable> = []
func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
return session
.dataTaskPublisher(for: request)
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
Everything is going in CalendarViewController from CalendarKit library which stands as follow:
import SwiftUI
import UIKit
class CalendarViewController: DayViewController {
convenience init(viewModel: CalendarScreen.ViewModel) {
self.init()
self.viewModel = viewModel
}
var viewModel = CalendarScreen.ViewModel()
var refresh: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
subscribeToNotification()
}
func subscribeToNotification() {
NotificationCenter.default.addObserver(
self, selector: #selector(eventChanged(_:)), name: .onDataImported, object: nil)
}
@objc func eventChanged(_ notification: Notification) {
print("notification")
reloadData()
}
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
// HOW CAN I WAIT FOR THIS LINE TO FINISH FETCH DATA FROM API
viewModel.fetchCalendarEvents()
//
let calendarKitEvents = viewModel.calendarEvents.filter {
dateTimeFormat.date(from: $0.start) ?? Date() >= date
&& dateTimeFormat.date(from: $0.end) ?? Date() <= date
}.map { item in
let event = Event()
event.dateInterval = DateInterval(
start: self.dateTimeFormat.date(from: item.start) ?? Date(),
end: self.dateTimeFormat.date(from: item.end) ?? Date())
event.color = UIColor(InvoiceColor(title: item.title))
event.isAllDay = false
event.text = item.title
return event
}
return calendarKitEvents
}
let dateTimeFormat: DateFormatter = {
let df = DateFormatter()
df.locale = Locale(identifier: "pl")
df.timeZone = TimeZone(abbreviation: "CET")
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return df
}()
}
And the SwiftUI and UIKit is bridged by UIViewControllerRepresntable
import SwiftUI
import UIKit
struct CalendarKitDisplayView: UIViewControllerRepresentable {
@ObservedObject var viewModel: CalendarScreen.ViewModel
func makeUIViewController(context: Context) -> DayViewController {
let dayViewCalendar = CalendarViewController(viewModel: viewModel)
return dayViewCalendar
}
func updateUIViewController(_ uiViewController: DayViewController, context: Context) {
}
}
And the entity CalendarEvent which is coded to CalendarKit event
public struct CalendarEvent: Codable, Identifiable {
public var id: Int = 0
var title: String = ""
var start: String = ""
var end: String = ""
var note: String?
}
My goal is to wait for viewModel.fetchCalendarEvents()
to fetch data from API and then start other tasks.
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
// HOW CAN I WAIT FOR THIS LINE TO FINISH FETCH DATA FROM API
viewModel.fetchCalendarEvents()
//
I tried to implement NotificationCenter
with variable refresh
but when i added and changed functions
To the CalendarViewController variable var refresh: Bool = false
and push notification to ViewModel
func fetchCalendarEvents() {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .eventChanged, object: nil)
})
}
After that i added subscribe to event in init() function in my CalendarViewController and #selector as follow
@objc func eventChanged(_ notification: Notification) {
print("notification")
refresh = true
reloadData()
}
I tried to add but it stay in infinite loop and variable never change
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
viewModel.fetchCalendarEvents()
while refresh == true {
}
}
I was thinking about using conclusion
or completion
handler but i am new in Swift programming and dont really know how it should looks like.
CodePudding user response:
Using a completion handler your function should look like this:
func fetchCalendarEvents(_ completion: @escaping () -> Void) {
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .eventChanged, object: nil)
completion()
})
}
And when calling it:
fetchCalendarEvents {
//finished, run some code.
}