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)")
}
}
}
Related
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.
I'd like to make an update detection system in my macOS SwiftUI app by pulling the latest release from GitHub via the API and then comparing the tag. How would I go about accessing the API from Swift? I've tried using the methods from here, medium.com, here, swifttom.com and here, steveclarkapps.com but none of them accomplish what I'm trying to do.
For the first method, the code functions with the provided example API, but doesn't work with the GitHub API and it returns this error instead:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
Method 2 suffers the same issue.
I couldn't even get enough of method 3's code working to try it.
Here's my adapted code based off of the medium.com method:
Model.swift
import Foundation
struct TaskEntry: Codable {
let id: Int
let tag_name: String
let name: String
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
/*if*/ let response = try! JSONDecoder().decode([TaskEntry].self, from: data) /*{*/
DispatchQueue.main.async {
self.results = response
}
return
/*}*/
}
}.resume()
}
}
Commented out code and variable names that seem irrelevant are just leftovers.
OS: macOS Big Sur 11.6
Xcode version: 13.0
Open this in your browser:
https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest
You will notice it is not an array but an object. You should be decoding an object like this:
JSONDecoder().decode(TaskEntry.self, from: data)
Edit:
This requires you to change your view. Notice this is no longer a List because you are no longer fetching an array but a single item:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var entry: TaskEntry? = nil
var body: some View {
VStack(alignment: .leading) {
if let entry = entry {
Text("\(entry.id)")
Text(entry.name)
Text(entry.tagName)
} else {
ProgressView()
}
}
.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases/latest") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(TaskEntry.self, from: data)
DispatchQueue.main.async {
self.entry = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}
NOTICE: I did some other improvements as well
Use JSONDecoder to convert from snake case to camel case
Added do catch block so your app doesn't crash
Check for errors before decoding
Added loading indicator (had to put something in the else)
However,
As our discussion you are probably calling the wrong endpoint. That endpoint is not returning an array but a single object, you can tell this because the JSON response begins with { rather than [
I've adjusted my answer to change the endpoint I believe you should be calling:
struct TaskEntry: Codable {
let id: Int
let tagName: String
let name: String
}
struct ContentView: View {
#State var results: [TaskEntry]? = nil
var body: some View {
if let results = results {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.name)
}
}
} else {
VStack(alignment: .leading) {
ProgressView()
.onAppear(perform: loadData)
}
}
}
func loadData() {
guard let url = URL(string: "https://api.github.com/repos/NCX-Programming/RNGTool/releases") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
// TODO: Handle data task error
return
}
guard let data = data else {
// TODO: Handle this
return
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode([TaskEntry].self, from: data)
DispatchQueue.main.async {
self.results = response
}
} catch {
// TODO: Handle decoding error
print(error)
}
}.resume()
}
}
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
First of all, this is my first attempt at Swift so I'm not really sure what I'm doing. I'm learning as I go right now and have hit a roadblock.
I'm trying to implement a WatchOS app that will call an API on a set timer to track fluctuations in some crypto prices.
I have figured out how to make the API call and get the JSON parsed to a point where I can print the data but I'm struggling to get it out of the closure and to my interface. I know the proper way to do this is with a completion handler but I can't seem to get a solid understanding of how to make that work in this scenario.
Any help would be appreciated
import SwiftUI
var refresh = bitcoin()
var btc: String = refresh
var eth: String = "ETH"
var doge: String = "DOGE"
struct ContentView: View {
var body: some View {
VStack(alignment: .leading ){
Label("\(btc)", image: "eth").padding(.vertical, 10.0)
Label("\(eth)", image: "eth").padding(.vertical, 10.0)
Label("\(doge)", image: "doge").padding(.vertical, 10.0)
}
.scaledToFill()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct responseData: Codable{
let data: Response?
}
struct Response: Codable{
var id: String
var rank: String
var symbol: String
var name: String
var supply: String
var maxSupply: String
var marketCapUsd: String
var volumeUsd24Hr: String
var priceUsd: String
var changePercent24Hr: String
var vwap24Hr: String
}
func bitcoin() -> String{
var result: String = "btc"
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
return
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
result = (response.data?.priceUsd)!
print(result)
}
task.resume()
return result
}
There many ways to achieve what you want, one way is to use "ObservableObject". Try something like this:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class CoinModel: ObservableObject {
#Published var btcPriceUsd = "not yet available"
#Published var ethPriceUsd = "not yet available"
#Published var dogePriceUsd = "not yet available"
}
struct ContentView: View {
#StateObject var coins = CoinModel()
var body: some View {
VStack(alignment: .leading ){
Label("\(coins.btcPriceUsd)", image: "btc").padding(.vertical, 10.0)
Label("\(coins.ethPriceUsd)", image: "eth").padding(.vertical, 10.0)
Label("\(coins.dogePriceUsd)", image: "doge").padding(.vertical, 10.0)
}
.scaledToFill()
.onAppear {
// bitcoin()
bitcoin2 { price in
coins.btcPriceUsd = price
}
}
}
func bitcoin() {
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
if let respData = response.data {
DispatchQueue.main.async {
coins.btcPriceUsd = respData.priceUsd
}
}
}
task.resume()
}
}
EDIT: if you really want to use completion, then try this:
func bitcoin2(completion: #escaping (String) -> Void) {
var request = URLRequest(url: URL(string: "https://api.coincap.io/v2/assets/bitcoin")!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return completion("")
}
let response = try! JSONDecoder().decode(responseData.self, from: data)
if let respData = response.data {
DispatchQueue.main.async {
completion(respData.priceUsd)
}
}
}
task.resume()
}
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()
}
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