Reputation: 817
I am trying to fetch data from the Unsplash API however I am getting the following error: "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
Here is the model struct:
// MARK: - UnsplashData
struct UnsplashData: Codable {
let id: String
let createdAt, updatedAt, promotedAt: Date
let width, height: Int
let color, blurHash: String
let unsplashDataDescription: String?
let altDescription: String
let urls: Urls
let links: UnsplashDataLinks
let categories: [String]
let likes: Int
let likedByUser: Bool
let currentUserCollections: [String]
let sponsorship: JSONNull?
let user: User
let exif: Exif
let location: Location
let views, downloads: Int
enum CodingKeys: String, CodingKey {
case id
case createdAt = "created_at"
case updatedAt = "updated_at"
case promotedAt = "promoted_at"
case width, height, color
case blurHash = "blur_hash"
case unsplashDataDescription = "description"
case altDescription = "alt_description"
case urls, links, categories, likes
case likedByUser = "liked_by_user"
case currentUserCollections = "current_user_collections"
case sponsorship, user, exif, location, views, downloads
}
}
// MARK: - Exif
struct Exif: Codable {
let make, model, exposureTime, aperture: String
let focalLength: String
let iso: Int
enum CodingKeys: String, CodingKey {
case make, model
case exposureTime = "exposure_time"
case aperture
case focalLength = "focal_length"
case iso
}
}
// MARK: - UnsplashDataLinks
struct UnsplashDataLinks: Codable {
let linksSelf, html, download, downloadLocation: String
enum CodingKeys: String, CodingKey {
case linksSelf = "self"
case html, download
case downloadLocation = "download_location"
}
}
// MARK: - Location
struct Location: Codable {
let title, name, city, country: String?
let position: Position
}
// MARK: - Position
struct Position: Codable {
let latitude, longitude: Double?
}
// MARK: - Urls
struct Urls: Codable {
let raw, full, regular, small: String
let thumb: String
}
// MARK: - User
struct User: Codable {
let id: String
let updatedAt: Date
let username, name, firstName, lastName: String
let twitterUsername: String?
let portfolioURL: String
let bio: String?
let location: String
let links: UserLinks
let profileImage: ProfileImage
let instagramUsername: String
let totalCollections, totalLikes, totalPhotos: Int
let acceptedTos: Bool
enum CodingKeys: String, CodingKey {
case id
case updatedAt = "updated_at"
case username, name
case firstName = "first_name"
case lastName = "last_name"
case twitterUsername = "twitter_username"
case portfolioURL = "portfolio_url"
case bio, location, links
case profileImage = "profile_image"
case instagramUsername = "instagram_username"
case totalCollections = "total_collections"
case totalLikes = "total_likes"
case totalPhotos = "total_photos"
case acceptedTos = "accepted_tos"
}
}
// MARK: - UserLinks
struct UserLinks: Codable {
let linksSelf, html, photos, likes: String
let portfolio, following, followers: String
enum CodingKeys: String, CodingKey {
case linksSelf = "self"
case html, photos, likes, portfolio, following, followers
}
}
// MARK: - ProfileImage
struct ProfileImage: Codable {
let small, medium, large: String
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public func hash(into hasher: inout Hasher) {
// No-op
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
And here is my ObservableObject:
class UnsplashAPI: ObservableObject {
enum State {
case loading
case loaded(UnsplashData)
}
@Published var state = State.loading
let url = URL(string: "https://api.unsplash.com/")!
func request() {
guard var components = URLComponents(url: url.appendingPathComponent("photos/random"),
resolvingAgainstBaseURL: true)
else {
fatalError("Couldn't append path component")
}
components.queryItems = [
URLQueryItem(name: "client_id", value: "vMDQ3Vzix8FN6MJL5Qpl3y0F7GdQsTtOjBe_L-IG2ro")
]
let request = URLRequest(url: components.url!)
let urlSession = URLSession(configuration: URLSessionConfiguration.default)
urlSession.dataTask(with: request) { data, urlResponse, error in
if let data = data {
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let response = try decoder.decode(UnsplashData.self, from: data)
self.state = .loaded(response) //error here
} catch {
print(error)
fatalError("Couldn't decode")
}
} else if let error = error {
print(error.localizedDescription)
} else {
fatalError("Didn't receive data")
}
}.resume()
}
}
Finally here is an example response I requested using Postman:
{
"id": "HWx5PYGudcI",
"created_at": "2020-12-08T22:11:11-05:00",
"updated_at": "2020-12-26T23:19:29-05:00",
"promoted_at": "2020-12-09T03:14:06-05:00",
"width": 4000,
"height": 6000,
"color": "#8ca6a6",
"blur_hash": "LAD,r_D*_M?^%ER4%$-oyYp0m+WE",
"description": null,
"alt_description": "boy in gray crew neck shirt",
"urls": {
"raw": "https://images.unsplash.com/photo-1607483421673-181fb79394b3?ixid=MXwxMTU5MTR8MHwxfHJhbmRvbXx8fHx8fHx8&ixlib=rb-1.2.1",
"full": "https://images.unsplash.com/photo-1607483421673-181fb79394b3?crop=entropy&cs=srgb&fm=jpg&ixid=MXwxMTU5MTR8MHwxfHJhbmRvbXx8fHx8fHx8&ixlib=rb-1.2.1&q=85",
"regular": "https://images.unsplash.com/photo-1607483421673-181fb79394b3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXwxMTU5MTR8MHwxfHJhbmRvbXx8fHx8fHx8&ixlib=rb-1.2.1&q=80&w=1080",
"small": "https://images.unsplash.com/photo-1607483421673-181fb79394b3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXwxMTU5MTR8MHwxfHJhbmRvbXx8fHx8fHx8&ixlib=rb-1.2.1&q=80&w=400",
"thumb": "https://images.unsplash.com/photo-1607483421673-181fb79394b3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXwxMTU5MTR8MHwxfHJhbmRvbXx8fHx8fHx8&ixlib=rb-1.2.1&q=80&w=200"
},
"links": {
"self": "https://api.unsplash.com/photos/HWx5PYGudcI",
"html": "https://unsplash.com/photos/HWx5PYGudcI",
"download": "https://unsplash.com/photos/HWx5PYGudcI/download",
"download_location": "https://api.unsplash.com/photos/HWx5PYGudcI/download"
},
"categories": [],
"likes": 51,
"liked_by_user": false,
"current_user_collections": [],
"sponsorship": null,
"user": {
"id": "3Bj-zCFL4-g",
"updated_at": "2020-12-26T14:58:36-05:00",
"username": "owensito",
"name": "Owen Vangioni",
"first_name": "Owen",
"last_name": "Vangioni",
"twitter_username": null,
"portfolio_url": null,
"bio": "Capturing magical moments...\nInstagram: @owensitens 18 years",
"location": "Argentina ",
"links": {
"self": "https://api.unsplash.com/users/owensito",
"html": "https://unsplash.com/@owensito",
"photos": "https://api.unsplash.com/users/owensito/photos",
"likes": "https://api.unsplash.com/users/owensito/likes",
"portfolio": "https://api.unsplash.com/users/owensito/portfolio",
"following": "https://api.unsplash.com/users/owensito/following",
"followers": "https://api.unsplash.com/users/owensito/followers"
},
"profile_image": {
"small": "https://images.unsplash.com/profile-1583211530737-0c1a46227535image?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32",
"medium": "https://images.unsplash.com/profile-1583211530737-0c1a46227535image?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64",
"large": "https://images.unsplash.com/profile-1583211530737-0c1a46227535image?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128"
},
"instagram_username": "owensitens",
"total_collections": 1,
"total_likes": 13,
"total_photos": 135,
"accepted_tos": true
},
"exif": {
"make": "NIKON CORPORATION",
"model": "NIKON D3300",
"exposure_time": "1/320",
"aperture": "5.3",
"focal_length": "45.0",
"iso": 200
},
"location": {
"title": null,
"name": null,
"city": null,
"country": null,
"position": {
"latitude": null,
"longitude": null
}
},
"views": 536045,
"downloads": 1267
}
Upvotes: 2
Views: 4635
Reputation: 4239
You will need to switch thread to the main thread from which you are allowed (and only from it!) to make UI changes in iOS. To fix the error you will need to use GCD and simply wrap the line where you change your state in the async
closure block.
DispatchQueue.main.async {
self.state = .loaded(response) // error should not be triggered anymore
}
Upvotes: 7