#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/HEAD5. Я не знаю, но не у всех хорошее соединение. Лучше перестраховаться, чем потом сожалеть. Кстати, время ожидания не имеет ничего общего со временем, необходимым для завершения загрузки
Ответ №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 в единый протокол просмотра, что значительно упрощает наш код». Итак, я попробую добавить этот код в свой связанный вид. Еще раз спасибо!