I kind of wrote everything correctly and the code itself is working but it gives me an error Result of call to 'fetchPokemon()' is unused, what could be the problem here?
Hear is my code: ModelView class
import Foundation
import Combine
class NetworkManager: ObservableObject {
let baseuRL = "https://pokeapi.co/api/v2/pokemon"
#Published var pokemon: [Pokemon] = []
var error: Error?
var cancellables: Set<AnyCancellable> = []
func fetchPokemon() -> Future<[Pokemon], Error> {
return Future<[Pokemon], Error> { promice in
guard let url = URL(string: "\(self.baseuRL)") else {
return promice(.failure(ApiError.unknowed))
}
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { (data, response) -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw ApiError.responseError
}
return data
}
.decode(type: PokemonList.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
}, receiveValue: {
promice(.success($0.results))
})
.store(in: &self.cancellables)
}
}
struct ContentView: View {
#StateObject var net = NetworkManager()
var body: some View {
List(net.pokemon, id: \.self) { pokemon in
Text(pokemon.name)
}.onAppear {
net.fetchPokemon()
}
}
}
Your fetchPokemon function returns a Future, but you're not doing anything with it -- that's why you're getting the unused error.
Also, in that function, you're returning your promise, but not doing anything with the results. So, you need to handle the Future and do something with those results.
It might look something like the following:
class NetworkManager: ObservableObject {
let baseuRL = "https://pokeapi.co/api/v2/pokemon"
#Published var pokemon: [Pokemon] = []
var error: Error?
var cancellables: Set<AnyCancellable> = []
//New function here:
func runFetch() {
fetchPokemon().sink { (completion) in
//handle completion, error
} receiveValue: { (pokemon) in
self.pokemon = pokemon //do something with the results from your promise
}.store(in: &cancellables)
}
private func fetchPokemon() -> Future<[Pokemon], Error> {
return Future<[Pokemon], Error> { promice in
guard let url = URL(string: "\(self.baseuRL)") else {
return promice(.failure(ApiError.unknowed))
}
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { (data, response) -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw ApiError.responseError
}
return data
}
.decode(type: PokemonList.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
}, receiveValue: {
promice(.success($0.results))
})
.store(in: &self.cancellables)
}
}
}
struct ContentView: View {
#StateObject var net = NetworkManager()
var body: some View {
List(net.pokemon, id: \.self) { pokemon in
Text(pokemon.name)
}.onAppear {
net.runFetch() //call runFetch instead of fetchPokemon
}
}
}
Since you didn't include the code for PokemonList I made an assumption about it's content:
struct PokemonList: Codable {
var results: [Pokemon]
}
If the type is different, you'll have to change what happens in receiveValue in runFetch.
Related
trying to fetch some data with dataTaskPublisher. however, constantly receive following log. it works every once in a while and not sure what's the difference. change URL does not make a difference. still only occasionally succeed the request.
Test2: receive subscription: (TryMap)
Test2: request unlimited
Test2: receive cancel
class DataSource: NSObject, ObservableObject {
var networker: Networker = Networker()
func fetch() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
fatalError("Invalid URL")
}
networker.fetchUrl(url: url)
}
}
class Networker: NSObject, ObservableObject {
var pub: AnyPublisher<Data, Error>? = nil
var sub: Cancellable? = nil
var data: Data? = nil
var response: URLResponse? = nil
func fetchUrl(url: URL) {
guard let url = URL(string: "https://apple.com") else {
return
}
pub = URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.tryMap() { data, response in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.print("Test2")
.eraseToAnyPublisher()
sub = pub?.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
fatalError(error.localizedDescription)
}
},
receiveValue: {
print($0)
}
)
}
add .store(in: &subscriptions)
I have a code, that should match 2 JSON files and connect all data in one struct. I'm pretty sure that It works just fine, but I have faced very strange problem. I want to make a picker:
import SwiftUI
struct CurrencyView: View {
#ObservedObject var api = CurrencyViewModel()
#State private var pickerSelection1 = 1
var body: some View {
Text("f")
Picker("", selection: $pickerSelection1) {
ForEach(0..<self.api.currencies.fullName.count) { // Error here
let currency = api.currencies.fullName[$0] // and here
Text(currency)
}
}
.id(UUID())
.labelsHidden()
.padding(.leading)
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView()
}
}
It shows this error:
Value of type '[Currency]' has no member 'fullName'
I know I'm missing smth and feel stupid, because I can't understand why. Thanks for the reply!)
Adding the rest of the code:
// Model
import Foundation
struct CurrencyModel: Codable {
var results: [String:Double]
}
struct CurrencyNewModel: Codable {
var currencies: [String:String]
}
struct Currency: Decodable {
let currencyCode: String
let fullName: String
var price: Double
}
// View Model
import SwiftUI
class CurrencyViewModel: ObservableObject {
#Published var currencies: [Currency] = []
init() {
fetchNewData { [self] (currency) in
switch currency {
case .success(let names):
print("Success")
DispatchQueue.main.async {
self.currencies = names.currencies.map {
Currency(currencyCode: $0.key, fullName: $0.value, price: 0)
}
}
fetchData { result in
switch result {
case .success(let prices):
print("Success")
for (index, value) in currencies.enumerated() {
if let price = prices.results.first(where: { $0.key == value.currencyCode }) {
DispatchQueue.main.async {
currencies[index].price = price.value
}
}
}
case .failure(let error):
print(error)
}
}
case .failure(let error):
print("Error", error)
}
}
}
func fetchData(completion: #escaping (Result<CurrencyModel,Error>) -> ()) {
guard let url = URL(string: "https://api.fastforex.io/fetch-all?from=USD&api_key=7ffe65c2ef-926f01d9e8-r7eql2") else { return }
URLSession.shared.dataTask(with: url) { data, responce, error in
if let error = error {
completion(.failure(error))
return
}
guard let safeData = data else { return }
do {
let currency = try JSONDecoder().decode(CurrencyModel.self, from: safeData)
completion(.success(currency))
}
catch {
completion(.failure(error))
}
}
.resume()
}
func fetchNewData(completion: #escaping (Result<CurrencyNewModel,Error>) -> ()) {
guard let url = URL(string: "https://api.fastforex.io/currencies?api_key=7ffe65c2ef-926f01d9e8-r7eql2") else { return }
URLSession.shared.dataTask(with: url) { data, responce, error in
if let error = error {
completion(.failure(error))
return
}
guard let safeData = data else { return }
do {
let currency = try JSONDecoder().decode(CurrencyNewModel.self, from: safeData)
completion(.success(currency))
}
catch {
completion(.failure(error))
}
}
.resume()
}
}
P.S. If you want to see the API, check the links in fetchData and fetchNewData, It's a free trial, so doesn't matter
Your error says:
Value of type '[Currency]' has no member 'fullName'
So it seems that api.currencies is an array – the array itself has no member fullName, only one single element of it has.
try this:
ForEach(api.currencies, id:\.currencyCode) { currency in
Text(currency.fullName)
}
I've been trying out Swift/SwiftUI for the first time, so I decided to make a small Hacker News client. I seem to be able to get the list of ids of the top stories back fine, but once the dispatchGroup gets involved, nothing works. What am I doing wrong?
Data.swift
import SwiftUI
struct HNStory: Codable, Identifiable {
var id: UInt
var title: String
var score: UInt
}
class Fetch {
func getStory(id: Int, completion: #escaping (HNStory) -> ()) {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")
URLSession.shared.dataTask(with: url!) { (data, _, _) in
let story = try!JSONDecoder().decode(HNStory.self, from: data!)
print(id, story)
DispatchQueue.main.async {
completion(story)
}
}
}
func getStories(completion: #escaping ([HNStory]) -> ()) {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")
var stories: [HNStory] = []
print("here")
URLSession.shared.dataTask(with: url!) { (data, _, _) in
var ids = try!JSONDecoder().decode([Int].self, from: data!)
ids = Array(ids[0...10])
print(ids)
let dispatchGroup = DispatchGroup()
for id in ids {
dispatchGroup.enter()
self.getStory(id: id) { (story) in
stories.append(story)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
print("Completed work")
DispatchQueue.main.async {
completion(stories)
}
}
}.resume()
}
}
ContentView.swift (probably doesn't matter, but just in case)
import SwiftUI
struct ContentView: View {
#State private var stories: [HNStory] = []
var body: some View {
Text("Hacker News").font(.headline)
List(stories) { story in
VStack {
Text(story.title)
Text(story.score.description)
}
}.onAppear{
Fetch().getStories { (stories) in
self.stories = stories
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
One major problem is this line:
Fetch().getStories...
Networking takes time. You make a Fetch instance and immediately let it destroy itself. Thus it does not survive long enough to do any networking! You need to configure a singleton that persists.
Another problem, as OOPer points out in a comment, is that your getStory creates a data task but never tells it to resume — so that method is doing no networking at all, even if it did have time to do so.
FWIW, with Swift UI, I would suggest you consider using Combine publishers for your network requests.
So, the publisher to get a single story:
func storyUrl(for id: Int) -> URL {
URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!
}
func hackerNewsStoryPublisher(for identifier: Int) -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: storyUrl(for: identifier))
.map(\.data)
.decode(type: HNStory.self, decoder: decoder)
.eraseToAnyPublisher()
}
And a publisher for a sequence of the above:
func hackerNewsIdsPublisher(for ids: [Int]) -> AnyPublisher<HNStory, Error> {
Publishers.Sequence(sequence: ids.map { hackerNewsStoryPublisher(for: $0) })
.flatMap(maxPublishers: .max(4)) { $0 }
.eraseToAnyPublisher()
}
Note, the above constrains it to four at a time, enjoying the performance gains of concurrency, but limiting it so you do not risk having latter requests time out:
Anyway, here is the first fetch of the array of ids and then launching the above:
let mainUrl = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")!
func hackerNewsPublisher() -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: mainUrl)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.flatMap { self.hackerNewsIdsPublisher(for: $0) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
(Now, you could probably cram all of the above into a single publisher, but I like to keep them small, so each individual publisher is very easy to reason about.)
So, pulling that all together, you have a view model like so:
import Combine
struct HNStory: Codable, Identifiable {
var id: UInt
var title: String
var score: UInt
}
class ViewModel: ObservableObject {
#Published var stories: [HNStory] = []
private let networkManager = NetworkManager()
private var request: AnyCancellable?
func fetch() {
request = networkManager.hackerNewsPublisher().sink { completion in
if case .failure(let error) = completion {
print(error)
}
} receiveValue: {
self.stories.append($0)
}
}
}
class NetworkManager {
private let decoder = JSONDecoder()
let mainUrl = URL(string: "https://hacker-news.firebaseio.com/v0/topstories.json")!
func storyUrl(for id: Int) -> URL {
URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!
}
func hackerNewsPublisher() -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: mainUrl)
.map(\.data)
.decode(type: [Int].self, decoder: decoder)
.flatMap { self.hackerNewsIdsPublisher(for: $0) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
// publisher for array of news stories, processing max of 4 at a time
func hackerNewsIdsPublisher(for ids: [Int]) -> AnyPublisher<HNStory, Error> {
Publishers.Sequence(sequence: ids.map { hackerNewsStoryPublisher(for: $0) })
.flatMap(maxPublishers: .max(4)) { $0 }
.eraseToAnyPublisher()
}
// publisher for single news story
func hackerNewsStoryPublisher(for identifier: Int) -> AnyPublisher<HNStory, Error> {
URLSession.shared.dataTaskPublisher(for: storyUrl(for: identifier))
.map(\.data)
.decode(type: HNStory.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
And your main ContentView is:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Hacker News").font(.headline)
List(viewModel.stories) { story in
VStack {
Text(story.title)
Text(story.score.description)
}
}.onAppear {
viewModel.fetch()
}
}
}
By calling Fetch().getStories, the Fetch class goes out of scope immediately and isn't retained.
I'd recommend making Fetch an ObservableObject and setting it as a property on your view:
#StateObject var fetcher = Fetch()
Then, call:
fetcher.getStories {
self.stories = stories
}
If you wanted to get even more SwiftUI-ish with it, you may want to look into #Published properties on ObservableObjects and how you can make your view respond to them automatically. By doing this, you could avoid having a #State variable on your view at all, not have to have a callback function, and instead just load the stories into a #Published property on the ObservableObject. Your view will be re-rendered when the #Published property changes. More reading: https://www.hackingwithswift.com/quick-start/swiftui/observable-objects-environment-objects-and-published
I am trying to learn Combine and it is a PITA for me. I never learned RX Swift, so this is all new to me. I am sure I am missing something simple with this one, but hoping for some help.
I am trying to fetch some JSON from an API and load it in a List view. I have a view model that conforms to ObservableObject and updates a #Published property which is an array. I use that VM to load my list, but it looks like the view loads way before this API returns (List showing up blank). I was hoping these property wrappers would do what I thought they were supposed to and re-render the view whenever the object changed.
Like I said, I am sure I am missing something simple. If you can spot it, I would love the help. Thanks!
class PhotosViewModel: ObservableObject {
var cancellable: AnyCancellable?
#Published var photos = Photos()
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: defaultPhotosObject)
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}
}
struct PhotoListView: View {
#EnvironmentObject var photosViewModel: PhotosViewModel
var body: some View {
NavigationView {
List(photosViewModel.photos) { photo in
NavigationLink(destination: PhotoDetailView(photo)) {
PhotoRow(photo)
}
}.navigationBarTitle("Photos")
}
}
}
struct PhotoRow: View {
var photo: Photo
init(_ photo: Photo) {
self.photo = photo
}
var body: some View {
HStack {
ThumbnailImageLoadingView(photo.coverPhoto.urls.thumb)
VStack(alignment: .leading) {
Text(photo.title)
.font(.headline)
Text(photo.user.firstName)
.font(.body)
}
.padding(.leading, 5)
}
.padding(5)
}
}
Based on your updated solution, here are some improvement suggestions (that wont fit in a comment).
PhotosViewModel improvement suggestions
Might I just make the recommendation of changing your load function from returning Void (i.e. returning nothing), to be returning AnyPublisher<Photos, Never> and skipping the last step .assign(to:on:).
One advantage of this is that your code takes one step toward being testable.
Instead of replaceError with some default value you can use catch together with Empty(completeImmediately: <TRUE/FALSE>). Because is it always possible to come up with any relevant default value? Maybe in this case? Maybe "empty photos"? If so then you can either make Photos conform to ExpressibleByArrayLiteral and use replaceError(with: []) or you can create a static var named empty, allowing for replaceError(with: .empty).
To sum up my suggestions in a code block:
public class PhotosViewModel: ObservableObject {
#Published var photos = Photos()
// var cancellable: AnyCancellable? -> change to Set<AnyCancellable>
private var cancellables = Set<AnyCancellable>()
private let urlSession: URLSession
public init(urlSession: URLSession = .init()) {
self.urlSession = urlSession
}
}
private extension PhotosViewModel {}
func populatePhotoCollection(named nameOfPhotoCollection: String) {
fetchPhotoCollection(named: nameOfPhotoCollection)
.assign(to: \.photos, on: self)
.store(in: &cancellables)
}
func fetchPhotoCollection(named nameOfPhotoCollection: String) -> AnyPublisher<Photos, Never> {
func emptyPublisher(completeImmediately: Bool = true) -> AnyPublisher<Photos, Never> {
Empty<Photos, Never>(completeImmediately: completeImmediately).eraseToAnyPublisher()
}
// This really ought to be moved to some APIClient
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return emptyPublisher()
}
return urlSession.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.catch { error -> AnyPublisher<Photos, Never> in
print("☣️ error decoding: \(error)")
return emptyPublisher()
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
*Client suggestion
You might want to write some kind of HTTPClient/APIClient/RESTClient and take a look at HTTP Status codes.
This is a highly modular (and one might argue - overly engineered) solution using a DataFetcher and a DefaultHTTPClient conforming to a HTTPClient protocol:
DataFetcher
public final class DataFetcher {
private let dataFromRequest: (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>
public init(dataFromRequest: #escaping (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>) {
self.dataFromRequest = dataFromRequest
}
}
public extension DataFetcher {
func fetchData(request: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
dataFromRequest(request)
}
}
// MARK: Convenience init
public extension DataFetcher {
static func urlResponse(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
_ dataAndUrlResponsePublisher: #escaping (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError>
) -> DataFetcher {
DataFetcher { request in
dataAndUrlResponsePublisher(request)
.mapError { HTTPError.NetworkingError.urlError($0) }
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPError.NetworkingError.invalidServerResponse(response)
}
headerInterceptor?(httpResponse.allHeaderFields)
guard case 200...299 = httpResponse.statusCode else {
badStatusCodeInterceptor?(UInt(httpResponse.statusCode))
let dataAsErrorMessage = errorMessageFromDataMapper.errorMessage(from: data) ?? "Failed to decode error from data"
print("⚠️ bad status code, error message: <\(dataAsErrorMessage)>, httpResponse: `\(httpResponse.debugDescription)`")
throw HTTPError.NetworkingError.invalidServerStatusCode(httpResponse.statusCode)
}
return data
}
.mapError { castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.eraseToAnyPublisher()
}
}
// MARK: From URLSession
static func usingURLSession(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
urlSession: URLSession = .shared
) -> DataFetcher {
.urlResponse(
errorMessageFromDataMapper: errorMessageFromDataMapper,
headerInterceptor: headerInterceptor,
badStatusCodeInterceptor: badStatusCodeInterceptor
) { urlSession.dataTaskPublisher(for: $0).eraseToAnyPublisher() }
}
}
HTTPClient
public final class DefaultHTTPClient {
public typealias Error = HTTPError
public let baseUrl: URL
private let jsonDecoder: JSONDecoder
private let dataFetcher: DataFetcher
private var cancellables = Set<AnyCancellable>()
public init(
baseURL: URL,
dataFetcher: DataFetcher,
jsonDecoder: JSONDecoder = .init()
) {
self.baseUrl = baseURL
self.dataFetcher = dataFetcher
self.jsonDecoder = jsonDecoder
}
}
// MARK: HTTPClient
public extension DefaultHTTPClient {
func perform(absoluteUrlRequest urlRequest: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
return Combine.Deferred {
return Future<Data, HTTPError.NetworkingError> { [weak self] promise in
guard let self = self else {
promise(.failure(.clientWasDeinitialized))
return
}
self.dataFetcher.fetchData(request: urlRequest)
.sink(
receiveCompletion: { completion in
guard case .failure(let error) = completion else { return }
promise(.failure(error))
},
receiveValue: { data in
promise(.success(data))
}
).store(in: &self.cancellables)
}
}.eraseToAnyPublisher()
}
func performRequest(pathRelativeToBase path: String) -> AnyPublisher<Data, HTTPError.NetworkingError> {
let url = URL(string: path, relativeTo: baseUrl)!
let urlRequest = URLRequest(url: url)
return perform(absoluteUrlRequest: urlRequest)
}
func fetch<D>(urlRequest: URLRequest, decodeAs: D.Type) -> AnyPublisher<D, HTTPError> where D: Decodable {
return perform(absoluteUrlRequest: urlRequest)
.mapError { print("☢️ got networking error: \($0)"); return castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.mapError { HTTPError.networkingError($0) }
.decode(type: D.self, decoder: self.jsonDecoder)
.mapError { print("☢️ 🚨 got decoding error: \($0)"); return castOrKill(instance: $0, toType: DecodingError.self) }
.mapError { Error.serializationError(.decodingError($0)) }
.eraseToAnyPublisher()
}
}
Helpers
public protocol ErrorMessageFromDataMapper {
func errorMessage(from data: Data) -> String?
}
public enum HTTPError: Swift.Error {
case failedToCreateRequest(String)
case networkingError(NetworkingError)
case serializationError(SerializationError)
}
public extension HTTPError {
enum NetworkingError: Swift.Error {
case urlError(URLError)
case invalidServerResponse(URLResponse)
case invalidServerStatusCode(Int)
case clientWasDeinitialized
}
enum SerializationError: Swift.Error {
case decodingError(DecodingError)
case inputDataNilOrZeroLength
case stringSerializationFailed(encoding: String.Encoding)
}
}
internal func castOrKill<T>(
instance anyInstance: Any,
toType expectedType: T.Type,
_ file: String = #file,
_ line: Int = #line
) -> T {
guard let instance = anyInstance as? T else {
let incorrectTypeString = String(describing: Mirror(reflecting: anyInstance).subjectType)
fatalError("Expected variable '\(anyInstance)' (type: '\(incorrectTypeString)') to be of type `\(expectedType)`, file: \(file), line:\(line)")
}
return instance
}
This ended up being an issue with my Codable struct not being set up properly. Once I added a default object in the .replaceError method instead of a blank array (Thanks #Asperi), I was able to see the decoding error and fix it. Works like a charm now!
Original:
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}
Updated:
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: defaultPhotosObject)
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}
Is there a way to specify that count should only publish on the main thread? I've seen docs that talk about setting up your Publisher using receive(on:), but in this case the #Publisher wrapper hides that logic.
import SwiftUI
import Combine
class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() { }
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.foregroundColor(Color.white)
.bold()
}
}
}
}
private func increment() {
NetworkUtils.count()
}
}
public class NetworkUtils {
public static func count() {
guard let url = URL.parse("https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
let statusCode = response.statusCode
if statusCode >= 200 && statusCode < 300 {
do {
guard let responseData = data else {
return
}
guard let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else {
return
}
if let newCount = json["new_count"] as? Int{
MyCounter.shared.count = newCount
}
} catch {
print("Caught error")
}
}
}
}
task.resume()
}
}
As you can see from my updated example, This is a simple SwiftUI view that has a button that when clicked makes a network call. The network call is asynchronous. When the network call returns, the ObservableObject MyCounter is updated on a background thread. I would like to know if there is a way to make the ObservableObject publish the change on the main thread. The only way I know to accomplish this now is to wrap the update logic in the network call closure like this:
DispatchQueue.main.async {
MyCounter.shared.count = newCount
}
Instead of using URLSession.shared.dataTask(with: request), you can use URLSession.shared.dataTaskPublisher(for: request) (docs) which will allow you to create a Combine pipeline. Then you can chain the receive(on:) operator as part of your pipeline.
URLSession.shared.dataTaskPublisher(for: request)
.map { response in ... }
...
.receive(on: RunLoop.main)
...
Also check out heckj's examples, I've found them to be very useful.
If you try to set value marked #Published from a background thread you will see this error:
Publishing changes from background threads is not allowed; make sure
to publish values from the main thread (via operators like receive
So you have to make sure anywhere you set the value that it this done on the main thread, the values will always be published on the main thread.
The Combine way to accomplish this (for API that do not provide Publishers) could be replacing
MyCounter.shared.count = newCount
with
Just(newCount).receive(on: RunLoop.main).assign(to: &MyCounter.shared.$count)
And here is how we can do it using Modern Concurrency async/await syntax.
import SwiftUI
final class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() {}
#MainActor func setCount(_ newCount: Int) {
count = newCount
}
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.bold()
}
}
.buttonStyle(.bordered)
}
}
private func increment() {
Task {
await NetworkUtils.count()
}
}
}
class NetworkUtils {
static func count() async {
guard let url = URL(string: "https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
do {
let (data, response) = try await URLSession.shared.data(for: request)
await MyCounter.shared.setCount(Int.random(in: 0...100)) // FIXME: Its just for demo
if let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 { throw URLError(.badServerResponse) }
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Expected [String: Any]"))
}
if let newCount = json["new_count"] as? Int {
await MyCounter.shared.setCount(newCount)
}
} catch {
print("Caught error :\(error.localizedDescription)")
}
}
}