Как использовать результаты из обработчика быстрого завершения?

#swift #xcode #macos #completionhandler

#swift #xcode #macos #обработчик завершения

Вопрос:

Я новичок в Swift и SwiftUI.

В моем проекте macOS SwiftUI я пытаюсь проверить, доступен ли URL-адрес, чтобы я мог представить одно из двух представлений условно. Одно представление, которое загружает URL-адрес изображения, другое, которое отображает изображение ошибки, если URL недоступен.

Вот мое расширение URL с завершением:

 import Foundation

extension URL {
    func isReachable(completion: @escaping (Bool) -> Void) {
        var request = URLRequest(url: self)
        request.httpMethod = "HEAD"
        request.timeoutInterval = 1.0
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if error != nil {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
            if let httpResp: HTTPURLResponse = response as? HTTPURLResponse {
                DispatchQueue.main.async {
                    completion(httpResp.statusCode == 200)
                }
                return
            } else {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
        }.resume()
    }
}
  

В другом месте я пытаюсь использовать это в представлении модели:

 var imageURL: URL? {
    if let url = self.book.image_url {
        return URL(string: url)
    } else {
        return nil
    }
}

var imageURLIsReachable: Bool {
    if let url = self.imageURL {
        url.isReachable { result in
            return result  // Error: Cannot convert value of type 'Bool' to closure result type 'Void'
        }
    } else {
        return false
    }
}
  

Хотя Xcode показывает эту ошибку:

Cannot convert value of type 'Bool' to closure result type 'Void'

Что я делаю не так?

Комментарии:

1. Вы не можете возвращать значение синхронно с помощью вычисляемого свойства, которое получается асинхронно . В вашем обработчике завершения вы можете присвоить результат свойству и использовать другой механизм ( didSet или via @Published ) для изменения.

2. programmingios.net/returning-a-value-from-asynchronous-code — Вы не можете вернуть Bool, который зависит от url.isReachable по той же причине, по которой url.isReachable используется обработчик завершения: он асинхронный . Это означает, что это произойдет в будущем. Люди всегда говорят: «Я новичок в Swift» и т. Д. Но, Конечно, никто не новичок в идее, что вы не можете сделать сейчас что-то, что зависит от того, что произойдет в будущем.

3. Не связано с вашим вопросом, но время ожидания 1 секунды слишком мало, чтобы определить, доступен ли удаленный URL-адрес или нет. Значение по умолчанию для времени ожидания запроса URL составляет 1 минуту

4. @LeoDabus Я всего лишь выполняю HEAD запрос. Как вы думаете, мне нужно больше 1 секунды? developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD

5. Я не знаю, но не у всех хорошее соединение. Лучше перестраховаться, чем потом сожалеть. Кстати, время ожидания не имеет ничего общего со временем, необходимым для завершения загрузки

Ответ №1:

Я получил эту работу после прочтения некоторых комментариев здесь и проведения дополнительных исследований / экспериментов. Вот что я изменил:

В расширении URL я оставил его практически таким же, поскольку я нахожу его более читаемым таким образом. Я нажал timeoutInterval на параметр:

 // Extensions/URL.swift


import Foundation

extension URL {
    func isReachable(timeoutInterval: Double, completion: @escaping (Bool) -> Void) {
        var request = URLRequest(url: self)
        request.httpMethod = "HEAD"
        request.timeoutInterval = timeoutInterval
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if error != nil {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
            if let httpResp: HTTPURLResponse = response as? HTTPURLResponse {
                DispatchQueue.main.async {
                    completion(httpResp.statusCode == 200)
                }
                return
            } else {
                DispatchQueue.main.async {
                    completion(false)
                }
                return
            }
        }.resume()
    }
}
  

Я изменил свой BookViewModel , чтобы сделать два свойства @Published и использовал там расширение URL:

 // View Models/BookViewModel.swift

import Foundation

class BookViewModel: ObservableObject {
    @Published var book: Book
    @Published var imageURLIsReachable: Bool
    @Published var imageURL: URL?
    
    init(book: Book) {
        self.book = book
        self.imageURL = nil
        self.imageURLIsReachable = false
        if let url = book.image_url {
            self.imageURL = URL(string: url)
            self.imageURL!.isReachable(timeoutInterval: 1.0) { result in
                self.imageURLIsReachable = result
            }
        }
    }
    
    // Rest of properties...
}
  

Теперь я BookThumbnailView могу правильно отображать условные представления:

 // Views/BookThumbnailView.swift

import SwiftUI
import Foundation
import KingfisherSwiftUI

struct BookThumbnailView: View {
    @ObservedObject var viewModel: BookViewModel
        
    private var book: Book {
        viewModel.book
    }
    
    @ViewBuilder
    var body: some View {
        if let imageURL = self.viewModel.imageURL {
            if self.viewModel.imageURLIsReachable {
                KFImage(imageURL)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(maxWidth: 70)
                        .cornerRadius(8)
            } else {
                ErrorBookThumbnailView()
            }
        } else {
            DefaultBookThumbnailView()
        }
    }
}
  

Фух, это был настоящий учебный опыт. Спасибо всем, кто прокомментировал предложения и подсказал, где искать!

Ответ №2:

Проблема буквально заложена в строке return result , как сообщает вам Xcode. Когда вы создаете свою функцию func isReachable(completion: @escaping (Bool) -> Void) , вы сообщаете Xcode, что собираетесь ввести что-то типа (Bool) -> Void , который должен быть примерно таким func someFunction(input: Bool) -> Void .

Но когда вы используете замыкание для ввода обработчика завершения, вы вводите функцию в type Bool -> Bool . Удалите строку return result или измените тип завершения в вашем func isReachable(completion:) .

Редактировать:

И действительно, я не рекомендую возвращать асинхронный результат в вычисляемом свойстве, это может вызвать некоторые другие проблемы.

Я бы изменил его на что-то вроде:

 func isReachable(completion: @esacping (Bool) -> Void) {
    ...
}

func showResultView() {
    guard let url = imageURL else { 
        // handling if the imageURL is nil
        return
    }
    url.isReachable { result in
        // do something with the result
        if result {
            // show viewController A
        } else {
            // show viewController B
        }
    }
}

// call showResultView anywhere you want, lets say you want to show it whenever the viewController appear
override func viewDidAppear() {
    ...
    showResultView()
}
  

Комментарии:

1. Я пытался использовать этот код в теле представления, но, похоже, это не разрешено. Куда бы вы поместили этот код? Кроме того, пока у меня нет никаких ViewControllers; Я в основном использую MVVM. Но я открыт для решений, чтобы заставить это работать. Спасибо

2. Извините, я недостаточно четко объяснил это, код не вызывается непосредственно в теле представления. Пожалуйста, посмотрите мой отредактированный ответ.

3. Насколько я понимаю, MVVM фокусируется на разделении представления (пользовательского интерфейса) и модели (данных), не обязательно иметь какой-либо ViewController, поэтому я немного смущен «У меня нет никаких ViewControllers; Я в основном использую MVVM». действительно, в моем проектепри использовании MVVM есть ViewController, ViewModel, Model и некоторый View для повторного использования. Но в любом случае вызывайте его в жизненном цикле вашего представления или всякий раз, когда вам это нужно, но не вставляйте его напрямую в тело класса представления. Надеюсь, это немного прояснит ситуацию.

4. Я где-то читал «SwiftUI объединяет UIView и UIViewController в единый протокол просмотра, что значительно упрощает наш код». Итак, я попробую добавить этот код в свой связанный вид. Еще раз спасибо!