I'm trying to understand the Combine methodology of making a JSON network call. I'm
clearly missing something basic.
The closest I get fails with the URLSession cancelled.
class NoteDataStore: ObservableObject {
#Published var notes: [MyNote] = []
init() {
getWebserviceNotes()
}
func getWebserviceNotes() {
let pub = Webservice().fetchNotes()
.sink(receiveCompletion: {_ in}, receiveValue: { (notes) in
self.notes = notes
})
}
}
}//class
The data element:
struct MyNote: Codable, Identifiable {
let id = UUID()
var title: String
var url: String
var thumbnailUrl: String
static var placeholder: MyNote {
return MyNote(title: "No Title", url: "", thumbnailUrl: "")
}
}
The network setup:
class Webservice {
func fetchNotes() -> AnyPublisher<[MyNote], Error> {
let url = "https://jsonplaceholder.typicode.com/photos"
guard let notesURL = URL(string: url) else { fatalError("The URL is broken")}
return URLSession.shared.dataTaskPublisher(for: notesURL)
.map { $0.data }
.decode(type: [MyNote].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
The console output is:
Task <85208F00-BC24-44AA-B644-E0398FE263A6>.<1> finished with error
[-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled"
UserInfo={NSErrorFailingURLStringKey=https://jsonplaceholder.typicode.com/photos,
NSLocalizedDescription=cancelled,
NSErrorFailingURLKey=https://jsonplaceholder.typicode.com/photos}
Any guidance would be appreciated. Xcode 11.4
let pub = Webservice().fetchNotes()
this publisher is released on exit of scope, so make it member, like
private var publisher: AnyPublisher<[MyNote], Error>?
func getWebserviceNotes() {
self.publisher = Webservice().fetchNotes()
...
Based on Asperi's answer - you will also want to add:
var cancellable: AnyCancellable?
And then you can sink to get the data:
func getWebserviceNotes() {
self.publisher = Webservice().fetchNotes()
guard let pub = self.publisher else { return }
cancellable = pub
.sink(receiveCompletion: {_ in },
receiveValue: { (notes) in
self.notes = notes
})
}
Related
I am trying to make network requests to download data and put it into a UITableView. I am not sure what I am doing wrong but when I call viewModel.request() in the ViewController it is not populating the UITableView.
I left some of the code out to make it simple but the UITableView works fine when using URLSession, I just can't get it to work with Combine.
If I print viewModel.sections in viewDidLoad(), it prints an empty array:
ViewController
class ViewController: UIViewController {
var viewModel = TestViewModel()
override func viewDidLoad() {
super.viewDidLoad()
//TableView Setup
setUpTableView()
viewModel.request()
self.tableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.sections[section].sports.count
}
}
TestViewModel
class TestViewModel {
#Published var sections: [Sections] = []
var date: Date = Date()
var cancellables: AnyCancellable?
func request(){
let nfl = NetworkManager.download(endpoint: .nfl, date: date.query())
.decode(type: EventModel.self, decoder: JSONDecoder())
let nba = NetworkManager.download(endpoint: .nba, date: date.query())
.decode(type: EventModel.self, decoder: JSONDecoder())
let mlb = NetworkManager.download(endpoint: .mlb, date: date.query())
.decode(type: EventModel.self, decoder: JSONDecoder())
let nhl = NetworkManager.download(endpoint: .nhl, date: date.query())
.decode(type: EventModel.self, decoder: JSONDecoder())
self.cancellables = Publishers.Zip4(nfl, nba, mlb, nhl)
.eraseToAnyPublisher()
.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print("Error:",error.localizedDescription)
}
}, receiveValue: { [weak self] (nfl, nba, mlb, nhl) in
var section: [Sections] = []
if !nfl.events.isEmpty {
section.append(.init(icon: "football.fill", title: "NFL", sports: nfl.events, leagues: nfl.leagues))
}
if !nba.events.isEmpty {
section.append(.init(icon: "basketball.fill", title: "NBA", sports: nba.events, leagues: nba.leagues))
}
if !mlb.events.isEmpty {
section.append(.init(icon: "baseball.fill", title: "MLB", sports: mlb.events, leagues: mlb.leagues))
}
if !nhl.events.isEmpty {
section.append(.init(icon: "hockey.puck.fill", title: "NHL", sports: nhl.events, leagues: nhl.leagues))
}
self?.sections = section
})
// This is Empty
print(self.sections)
}
}
NetworkManager
class NetworkManager {
static func download(endpoint: Endpoint, date: String) -> AnyPublisher<Data, Error> {
let url = URL(string: "***API URL***")!
let final = url.appendingPathComponent(endpoint.build())
var components = URLComponents(url: final, resolvingAgainstBaseURL: true)!
components.queryItems = [
URLQueryItem(name: "dates", value: date),
]
let request = URLRequest(url: components.url!)
return
URLSession.shared.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap{(data, response) -> Data in
guard let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
You’re using SwiftUI #Published method in UIKit. What works best is CurrentValueSubject for UIKit.
Change you sections to be var sections CurrentValueSubject<[Sections], Never>([]) in your VM. Then in your VM request function, build up the sections data (_values) and send by sections.send(_values)
In your controller you need to bind to the VM.sections by viewModel.sections.sink { in $0 } where $0 is your data.
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.
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.
}
I am using SwiftUI, UIKit and external library CalendarKit in my project. The problem is that events don't load while initializing on first load of calendar. When I change date in the in the nav, update event or create new event everything works fine. Events are reloading and are showed up on screen.
I have few classes in my projects. First is CalendarScreen which renders the SwiftUI view, ViewModel of CalendarScreen which loads data fetched from API. Service which provides repository and Repository class which runs URLRequest. The UIKit class of DayViewController where everything is going in:
class CalendarViewController: DayViewController {
convenience init(viewModel: CalendarScreen.ViewModel) {
self.init()
self.viewModel = viewModel
}
var viewModel = CalendarScreen.ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
reloadData()
}
override func reloadData() {
dayView.timelinePagerView.pagingViewController.children.forEach({ (controller) in
if let controller = controller as? TimelineContainerController {
controller.timeline.layoutAttributes = eventsForDate(Date()).map(EventLayoutAttributes.init)
}
})
}
override func eventsForDate(_ date: Date) -> [EventDescriptor] {
return viewModel.fetchCalendarEvents {
var calendarKitEvents = [Event()]
calendarKitEvents = self.viewModel.calendarEvents.compactMap { 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
}
self.eventsOnCeldanr = calendarKitEvents
}
}
}
And the classes corresponding to my APICall the main function is func fetchCalendarEvents which return Events to my Controller
class Agent {
let session = URLSession.shared
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()
}
}
struct CalendarRepository {
private let agent = Agent()
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")
return agent.run(request)
}
}
struct CalendarService {
private var calendarRepository = CalendarRepository()
func getEvents() -> AnyPublisher<[CalendarEvent], Error> {
return calendarRepository.getEvents()
}
extension CalendarScreen {
class ViewModel: ObservableObject {
let calendarService = CalendarService()
#Published var calendarEvents: [CalendarEvent]
#Published var events: [Event]
var cancellable: AnyCancellable?
init() {
self.calendarEvents = [CalendarEvent()]
self.events = []
}
func fetchCalendarEvents(_ completion: #escaping () -> Void)
-> [EventDescriptor]
{
cancellable = calendarService.getEvents()
.sink(
receiveCompletion: { _ in },
receiveValue: {
calendarEvents in self.calendarEvents = calendarEvents
NotificationCenter.default.post(name: .onEventLoaded, object: nil)
self.createEvents()
completion()
})
return events
}
func createEvents() {
self.events = self.calendarEvents.compactMap({ (item) in
var event = Event()
event.dateInterval = DateInterval(
start: self.dateTimeFormat.date(from: item.start)!,
end: self.dateTimeFormat.date(from: item.end)!)
event.color = UIColor(InvoiceColor(title: item.title))
event.isAllDay = false
event.text = item.title
return event
})
}
}
}
}
So the problem is when I load view for the first time the reloadDate() returning empty [] array.
While i try to debug Events are in variable calendarKitEvents but without sucessful return and on first load function ends on reciveCompletion call instead of reciveValue call in fetchCalendarEvents function.
I have the following classes that perform a network call -
import SwiftUI
import Combine
struct CoinsView: View {
private let coinsViewModel = CoinViewModel()
var body: some View {
Text("CoinsView").onAppear {
self.coinsViewModel.fetchCoins()
}
}
}
class CoinViewModel: ObservableObject {
private let networkService = NetworkService()
#Published var data = String()
var cancellable : AnyCancellable?
func fetchCoins() {
cancellable = networkService.fetchCoins().sink(receiveCompletion: { _ in
print("inside receive completion")
}, receiveValue: { value in
print("received value - \(value)")
})
}
}
class NetworkService: ObservableObject {
private var urlComponents : URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = "jsonplaceholder.typicode.com"
components.path = "/users"
return components
}
var cancelablle : AnyCancellable?
func fetchCoins() -> AnyPublisher<Any, URLError> {
return URLSession.shared.dataTaskPublisher(for: urlComponents.url!)
.map{ $0.data }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
What I want to achieve currently is just to print the JSON result.
This doesn't seem to work, and from debugging it never seems to go inside the sink{} method, therefor not executing it.
What am I missing?
After further investigation with Asperi's help I took the code to a clean project and saw that I have initialized a struct that wraps NSPersistentContainer which causes for some reason my network requests not to work. Here is the code, hopefully someone can explain why it prevented my networking to execute -
import SwiftUI
#main
struct BasicApplication: App {
let persistenceController = BasicApplciationDatabase.instance
#Environment(\.scenePhase)
var scenePhase
var body: some Scene {
WindowGroup {
CoinsView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .background:
print("Scene is background")
persistenceController.save()
case .inactive:
print("Scene is inactive")
case .active:
print("Scene is active")
#unknown default:
print("Scene is unknown default")
}
}
}
}
import CoreData
struct BasicApplciationDatabase {
static let instance = BasicApplciationDatabase()
let container : NSPersistentContainer
init() {
container = NSPersistentContainer(name: "CoreDataDatabase")
container.loadPersistentStores { NSEntityDescription, error in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
func save(completion : #escaping(Error?) -> () = {_ in} ){
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
completion(nil)
} catch {
completion(error)
}
}
}
func delete(_ object: NSManagedObject, completion : #escaping(Error?) -> () = {_ in} ) {
let context = container.viewContext
context.delete(object)
save(completion: completion)
}
}
Hi I'm fetching data from a local api in a LocationsDataService class and assigning this as a #Published var in the data service and then using this in my LocationsViewModel. If I wait for my api request to complete, for example;
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
let locations = dataService.locations
self.locations = locations
}
Then the locations are rendered on the screen.
This is my data service class;
//
// LocationsDataService.swift
// MapTest
//
//
import Foundation
import MapKit
import Combine
class LocationsDataService: ObservableObject {
let token = "2|asOnUG27uCrcVuGxOO65kS25mX0nUSls5ApfemQy";
#Published var locations: [Location] = []
public var cancellable: AnyCancellable?
enum HTTPError: LocalizedError {
case statusCode
}
init() {
fetch()
}
func fetch() {
guard let url = URL(string: "http://localhost:8080/api/locations") else {
print("Invalid url...")
return
}
var urlRequest = URLRequest(
url: url
)
urlRequest.setValue(
"Bearer \(token)",
forHTTPHeaderField: "Authorization"
)
self.cancellable = URLSession.shared.dataTaskPublisher(for: urlRequest)
.tryMap { output in
guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
throw HTTPError.statusCode
}
return output.data
}
.decode(type: [Location].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
fatalError(error.localizedDescription)
}
}, receiveValue: { locations in
self.locations = locations
})
}
}
And this is my view model
//
// LocationsViewModel.swift
// CoffeeShops
//
//
import Foundation
import MapKit
import SwiftUI
class LocationsViewModel: ObservableObject {
// all loaded locations
#Published var locations: [Location]
init() {
let dataService = LocationsDataService() // the init function will do the api call
self.locations = [Location(
name: "Amsterdam",
address: "Amsterdam",
latitude: 52.3721009,
longitude: 4.8912196,
description: "Amsterdam",
imageNames: [],
link: "https://en.wikipedia.org/wiki/Colosseum")]
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
let locations = dataService.locations
self.locations = locations
print(locations)
}
}
}
I'm not sure why I need to explicitly wait for the api before assigning it to the locations in the LocationsViewModel as the LocationsDataService.locations is #Published var locations: [Location] = [], therefore I thought it would be observed. Obviously it wouldn't be great to have to put in a time limit before I can update the screen as it could be done in 1 second or 5 seconds. Any ideas what I'm doing wrong?
ObservableObject will make SwiftUI view to update the view when #Published changes. But when you're creating one ObservableObject inside an other one, there's no way it can automatically work.
You don't actually need ObservableObject for your service, you only need #Published for you property, and and there's how you can sync it with your view model:
private var cancellable: AnyCancellable?
init() {
cancellable = dataService.$locations.sink(receiveValue: { locations in
self.locations = locations
})
}