How to use results from Swift completion handler? - swift

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?

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!

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

Related

How can I use the return from my function as an argument for another function in onAppear

I am trying to use the result of the following function:
class GetRefresh: ObservableObject {
let user = Auth.auth()
var ref = Database.database().reference()
#Published var refreshToken = ""
func getRefresh() {
ref.child("\(user.currentUser!.uid)").child("refreshUrl").getData { error, snapshot in
guard error == nil else {
print(error!.localizedDescription)
return
}
let refreshUrl = snapshot.value as? String ?? "Unknown"
let url = URL(string: refreshUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
URLSession.shared.dataTask(with:request) { (data, response, error) in
if error != nil {
print(error)
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
let token = parsedData["access_token"] as? String
self.refreshToken = token!
} catch let error as NSError {
print(error)
}
}
}.resume()
}
}
}
To pass as an argument to a Async service that is called in the onAppear of my view:
struct DevicesTab: View {
#StateObject var callDevices = CallDevices()
#StateObject var getRfrsh = GetRefresh()
var ref: DatabaseReference!
var body: some View {
VStack(spacing: 0) {
greeting.edgesIgnoringSafeArea(.top)
messages
Spacer()
}
.onAppear {
getRfrsh.getRefresh()
callDevices.getDevices(refreshToken: getRfrsh.refreshToken) //this fails
}
}
}
The callDevices.getDevices is using the argument as a Header for its URLRequest.
If i hardcode the token in onAppear like:
.onAppear {
getRfrsh.getRefresh()
callDevices.getDevices(refreshToken: "HardcodedToken") //this works
}
It works fine, albeit with a bit of a delay before the data is loaded but it works...
I am sure the function is returning correctly as I was able to print it in GetRefresh.
Well the reason for your code failing is you are not waiting for the network call to be completted before you call the next method.
Solution:
You could rework the code to be async. Or you could observe the changes of your #Publishedvar and call getDevices.
.onChange(of: getRfrsh.refreshToken) { newValue in
callDevices.getDevices(refreshToken: newValue)
}
.onAppear {
// to avoid repeatitive refreshing
if getRfrsh.refreshToken == ""{
getRfrsh.getRefresh()}
}
But be aware. This will call getDevicesevery time the value changes.

Returning the image of the LPMetadataProvider in a function in the LinkPresentation Framework in SwiftUI

I want to have a function return only the image of the LPLMetadataProvider().
let image = fetchRichLinkimage(url: urltofetch[0])
This is what i got sofar. I have an Url as input and UIImage as output of that function.
Inside the function it instantiates the LPMetadataProvider(), then calls the fetchingMetadata method to retrieve the metadata. Within the metadata.imageProvider it loads the UIImage and if that image exists it should get returned as the output of the function.
I am new to SwiftUI/Swift so i have trouble understanding how i can assign the result to the output of the function. If someone can tell me what to do that would be appreciated.
func fetchimage(url: String) -> UIImage {
return UIImage(systemName: "paperplane")!
}
func fetchRichLinkimage(url: String) -> UIImage {
let metadataProvider = LPMetadataProvider()
guard let urlURL = URL(string: url) else {
return UIImage(systemName: "paperplane")!
}
metadataProvider.startFetchingMetadata(for: urlURL) { (metadata, error) in
guard error == nil else {
return
}
guard let imageProvider = metadata?.imageProvider else { return }
imageProvider.loadObject(ofClass: UIImage.self) { (image, error) in
guard error == nil else {
return
}
if let image = image as? UIImage {
DispatchQueue.main.async {
return self.image
}
} else {
print("no image available")
}
}
}
}
Answer based on your comments reflecting the fact that this is for a widget:
The following is a very minimal example of loading the LPLinkMetadata and associated UIImage in a widget, using callbacks from the TimelineProvider:
import WidgetKit
import SwiftUI
import LinkPresentation
class LPLoader {
let metadataProvider = LPMetadataProvider()
func loadImage(url: URL, completion: #escaping (UIImage?) -> Void) {
metadataProvider.startFetchingMetadata(for: url) { (metadata, error) in
guard error == nil else {
assertionFailure("Error")
return completion(nil)
}
guard let imageProvider = metadata?.imageProvider else {
return completion(nil)
}
imageProvider.loadObject(ofClass: UIImage.self) { (image, error) in
guard error == nil else {
// handle error
return completion(nil)
}
if let image = image as? UIImage {
// do something with image
return completion(image)
} else {
print("no image available")
return completion(nil)
}
}
}
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), image: UIImage(systemName: "pencil")!)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
return completion(SimpleEntry(date: Date(), image: UIImage(systemName: "pencil")!))
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let loader = LPLoader()
loader.loadImage(url: URL(string: "https://google.com")!) { (image) in
guard let image = image else {
return completion(Timeline(entries: [], policy: .atEnd))
}
return completion(Timeline(entries: [.init(date: Date(), image: image)], policy: .atEnd))
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let image: UIImage
}
struct TestWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Image(uiImage: entry.image)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
LPLoader is a class that just has one function, which loads the metadata for a link and then fetches the image. Note that it currently doesn't have any error handling.
In the TimelineProvider, you provide the timeline by calling the completion, which allows you to do asynchronous tasks (which you can't do in the widget itself). Right now, the data flow is relatively simple because there's just one image getting fetched. If you were to make a timeline with multiple entries and had to wait for them to load separately, it would get more complicated. I'd look into using Combine to do the asynchronous tasks separately and calling a callback when they're all finished (probably outside the scope of this question, but worth posting another question if you need this feature and don't know how to implement it).
Once the metadata and image are loaded, the original completion method for the TimelineProvider is called. The entry itself just has a Date and the UIImage that's been loaded.

Object don't update after Api Call on SwiftUI

Im trying to do a simple SwiftUI App to fetch recent movies premiers
Trying to follow MVVM and found a couple of tutorials to make the Api calls but they put the Api calls on the model
So i so try to make a Service class And a State class to handle my #Observed objects the problem is when I try to see the movie details the details don't load, but when I try another movie the details are of the last movie
You can see the bug here
This is my Movies Service
public class MoviesService {
private let apiKey = "?api_key=" + "xxx"
private let baseAPIURL = "https://api.themoviedb.org/3/movie/"
private let language = "&language=" + "es-MX"
var nextPageToLoad = 1
var nowPlayingMovies = [Movie]()
var movieDetail : MovieDetail?
init() {
loadNowPlaying()
}
func loadNowPlaying(){
let urlString = "\(baseAPIURL)now_playing\(apiKey)\(language)&page=\(nextPageToLoad)"
print(urlString)
let url = URL(string: urlString)!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request, completionHandler:parseMovies(data:response:error:))
task.resume()
}
func parseMovies(data: Data?, response: URLResponse?, error: Error?){
var NowPlayingMoviesResult = [Movie]()
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(NowPlaying.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async { [self] in
// update our UI
NowPlayingMoviesResult = decodedResponse.results!
self.nextPageToLoad += 1
for movie in NowPlayingMoviesResult {
nowPlayingMovies.append(movie)
}
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}
func loadDetailMovie(id : Int) {
let urlString = String("\(baseAPIURL)\(id)\(apiKey)\(language)")
let url = URL(string: urlString)!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request, completionHandler:parseDetailMovie(data:response:error:))
task.resume()
}
func parseDetailMovie(data: Data?, response: URLResponse?, error: Error?){
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(moviesApp.MovieDetail.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
self.movieDetail = decodedResponse
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}
}
This is my State class
class StateController: ObservableObject, RandomAccessCollection {
typealias Element = Movie
#Published var movies = [Movie]()
#Published var movie : MovieDetail?
private let moviesService = MoviesService()
func shouldLoadMoreData(item : Movie? = nil) -> Bool {
if item == movies.last {
return true
}
return false
}
func reloadMovies(item : Movie? = nil){
DispatchQueue.main.async {
if self.shouldLoadMoreData(item: item) {
self.moviesService.loadNowPlaying()
}
self.movies = self.moviesService.nowPlayingMovies
}
}
func loadMovieDetails(id: Int){
DispatchQueue.main.async {
self.moviesService.loadDetailMovie(id: id)
self.movie = self.moviesService.movieDetail
}
}
var startIndex: Int { movies.startIndex }
var endIndex: Int { movies.endIndex }
subscript(position: Int ) -> Movie {
return movies[position]
}
}
And this is My Movie Detail View
import SwiftUI
struct MovieDetailView: View {
#EnvironmentObject private var stateController: StateController
#State var id : Int
var body: some View {
DetailMovieContent(movie: $stateController.movie)
.onAppear{
stateController.loadMovieDetails(id: id)
}
}
}
So this is my questions
Im doing a good Approach?
Whats the right way of make and Api call using MVVM and SwiftUI?
The full App is here

Calling function from view SwiftUI

I'm trying to call a function from view using SwiftUI. This view receive an String parameter from view that is calling it.
struct BookList: View {
var name: String
var body: some View {
let data: () = getData(from: self.name)
...
}
}
The function get data is consuming a rest service and getting some data.
func getData(from url: String){
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("Something went wrong")
return
}
//Have data
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
} catch {
print("failed to convert \(error.localizedDescription)")
}
guard let json = result else{
return
}
print("Page: \(json.page)")
print("Books: \(json.books.first)")
})
task.resume()
}
struct Response: Codable {
var error: String
var total: String
var page: String
var books: [MyBook]
}
The problem is that I don't know how to call this function when view start. In this sample I'm getting the error:
"Function declares an opaque return type, but has no return statements
in its body from which to infer an underlying type"
How can I fix it?
"Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type"
This specific error is because you have other statements besides Views in your body property. Typically, the body property will use an implicit return but if you include other statements—such as your call to getData—then you need to use an explicit return instead. Like so:
var body: some View {
let data: () = getData(from: self.name)
return ...your View(s)
}
You need to return data from getData. Because it's asynchronous, you need a completion handler:
func getData(from url: String, completion: #escaping (Response) -> Void) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
...
completion(json)
})
task.resume()
}
and call it in your view in .onAppear:
struct BookList: View {
var name: String
#State var data: Response?
var body: some View {
Text("Your view...")
.onAppear {
getData(from: self.name) { data in
DispatchQueue.main.async {
self.data = data
}
}
}
}
}

Consume #Published values on the main thread?

Is there a way to specify that count should only publish on the main thread? I've seen docs that talk about setting up your Publisher using receive(on:), but in this case the #Publisher wrapper hides that logic.
import SwiftUI
import Combine
class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() { }
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.foregroundColor(Color.white)
.bold()
}
}
}
}
private func increment() {
NetworkUtils.count()
}
}
public class NetworkUtils {
public static func count() {
guard let url = URL.parse("https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
let statusCode = response.statusCode
if statusCode >= 200 && statusCode < 300 {
do {
guard let responseData = data else {
return
}
guard let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else {
return
}
if let newCount = json["new_count"] as? Int{
MyCounter.shared.count = newCount
}
} catch {
print("Caught error")
}
}
}
}
task.resume()
}
}
As you can see from my updated example, This is a simple SwiftUI view that has a button that when clicked makes a network call. The network call is asynchronous. When the network call returns, the ObservableObject MyCounter is updated on a background thread. I would like to know if there is a way to make the ObservableObject publish the change on the main thread. The only way I know to accomplish this now is to wrap the update logic in the network call closure like this:
DispatchQueue.main.async {
MyCounter.shared.count = newCount
}
Instead of using URLSession.shared.dataTask(with: request), you can use URLSession.shared.dataTaskPublisher(for: request) (docs) which will allow you to create a Combine pipeline. Then you can chain the receive(on:) operator as part of your pipeline.
URLSession.shared.dataTaskPublisher(for: request)
.map { response in ... }
...
.receive(on: RunLoop.main)
...
Also check out heckj's examples, I've found them to be very useful.
If you try to set value marked #Published from a background thread you will see this error:
Publishing changes from background threads is not allowed; make sure
to publish values from the main thread (via operators like receive
So you have to make sure anywhere you set the value that it this done on the main thread, the values will always be published on the main thread.
The Combine way to accomplish this (for API that do not provide Publishers) could be replacing
MyCounter.shared.count = newCount
with
Just(newCount).receive(on: RunLoop.main).assign(to: &MyCounter.shared.$count)
And here is how we can do it using Modern Concurrency async/await syntax.
import SwiftUI
final class MyCounter: ObservableObject {
#Published var count = 0
public static let shared = MyCounter()
private init() {}
#MainActor func setCount(_ newCount: Int) {
count = newCount
}
}
struct ContentView: View {
#ObservedObject var state = MyCounter.shared
var body: some View {
return VStack {
Text("Current count: \(state.count)")
Button(action: increment) {
HStack(alignment: .center) {
Text("Increment")
.bold()
}
}
.buttonStyle(.bordered)
}
}
private func increment() {
Task {
await NetworkUtils.count()
}
}
}
class NetworkUtils {
static func count() async {
guard let url = URL(string: "https://www.example.com/counter") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
do {
let (data, response) = try await URLSession.shared.data(for: request)
await MyCounter.shared.setCount(Int.random(in: 0...100)) // FIXME: Its just for demo
if let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 { throw URLError(.badServerResponse) }
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Expected [String: Any]"))
}
if let newCount = json["new_count"] as? Int {
await MyCounter.shared.setCount(newCount)
}
} catch {
print("Caught error :\(error.localizedDescription)")
}
}
}