skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    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!


  2. The problem literally laid in the line return result, as Xcode tells you. When you create your function func isReachable(completion: @escaping (Bool) -> Void), you are telling Xcode that you are going to input something in the type of (Bool) -> Void, which should be something like func someFunction(input: Bool) -> Void.

    But when you use a closure to input the completion handler, you’re inputting a function in type Bool -> Bool. Remove the line return result, or change the type of the completion in your func isReachable(completion:).

    Edit:

    And indeed I don’t recommend returning a async result in a computed property, that would cause some other problem.

    I would change it to something like:

    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()
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search