"Publishing changes from background threads is not allowed" 使用 URLSession 获取数据时

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

我正在尝试从 Unsplash API 获取数据,但是我收到以下错误:“不允许从后台线程发布更改;确保从主线程发布值(通过像这样的运算符在模型更新时接收(on :))。

这是模型结构:

// 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()
    }
}

这是我的 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()
    }
}

最后,这是我使用 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
}

您需要将线程切换到 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 并简单地将更改状态的行换行到 async 闭包块中。

DispatchQueue.main.async {
    self.state = .loaded(response) // error should not be triggered anymore
}

在您的 class 定义之前添加 @MainActor。