Arnav Motwani
Arnav Motwani

Reputation: 817

"Publishing changes from background threads is not allowed" while fetching data using URLSession

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

Answers (2)

Disco
Disco

Reputation: 363

add @MainActor before your class definition.

Upvotes: 0

Witek Bobrowski
Witek Bobrowski

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

Related Questions