Search code examples
swiftxcodemacoscompletionhandler

How to use results from Swift completion handler?


I'm new to Swift and SwiftUI.

In my macOS SwiftUI project, I'm trying to verify that a URL is reachable so I can present one of two views conditionally. One view which loads the image URL, another one which displays an error image if the URL is not reachable.

Here's my URL extension with completion:

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

Elsewhere, I'm trying to use that in a model-view:

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
    }
}

Though Xcode is showing this error:

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

What am I doing wrong?


Solution

  • I got this to work after reading some of the comments here and doing more research/experimentation. Here's what I changed:

    In the URL extension, I left it pretty much the same as I find it more readable this way. I did push the timeoutInterval to a parameter:

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

    I modified my BookViewModel to make two of the properties to @Published and used the URL extension there:

    // 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...
    }
    

    Now my BookThumbnailView can properly display the conditional views:

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

    Whew, that was quite the learning experience. Thanks to everyone who commented with suggestions and provided hints on where to look!