Calling function from view SwiftUI - swift

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

Related

How to bind data to ViewModel for showing it on UI in MVVM?

In my app I am using MVVM pattern.
Below is my Model.
struct NewsModel: Codable {
let status: String
let totalResults: Int
let articles: [Article]
}
struct Article: Codable {
let source: Source
let author: String?
let title: String
let articleDescription: String?
let url: String
let urlToImage: String?
let publishedAt: Date
let content: String?
enum CodingKeys: String, CodingKey {
case source, author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
struct Source: Codable {
let id: String?
let name: String
}
Below is my ViewModel. Which is used for show the data from API.
struct NewsArticleViewModel {
let article: Article
var title:String {
return self.article.title
}
var publication:String {
return self.article.articleDescription!
}
var imageURL:String {
return self.article.urlToImage!
}
}
Below is my API request class.
class Webservice {
func getTopNews(completion: #escaping (([NewsModel]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let news = try? JSONDecoder().decode([NewsModel].self, from: data)
DispatchQueue.main.async {
completion(news)
}
}.resume()
}
}
After receiving response from my API I want to show it on screen. For this I added below ViewModel.
class NewsListViewModel: ObservableObject {
#Published var news: [NewsArticleViewModel] = [NewsArticleViewModel]()
func load() {
fetchNews()
}
private func fetchNews() {
Webservice().getTopNews {
news in
if let news = news {
//How to bind this data to NewsArticleViewModel and show it on UI?
}
}
}
}
Please let me know. What I have to write there for showing it on UI.
According to the documentation of newsapi.org your request will return one NewsModel object not an array. So change your Webservice class to:
class Webservice {
//Change the completion handler to return an array of Article
func getTopNews(completion: #escaping (([Article]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
// decode to a single NewsModel object instead of an array
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let news = try? decoder.decode(NewsModel.self, from: data)
DispatchQueue.main.async {
// completion with an optional array of Article
completion(news?.articles)
}
}.resume()
}
}
You would need to map those received values to NewsArticleViewModel types. For example:
Webservice().getTopNews { articles in
if let articles = articles {
self.news = articles.map{NewsArticleViewModel(article: $0)}
}
}
And remove let news: NewsModel from the NewsArticleViewModel struct as it is not needed.
Edit:
It seems:
let publishedAt: Date
is throwing an error. Jsondecoder fails to interpret the string to a date. Change your Webservice. I´ve updated it in my answer.
You could remove the legacy MVVM pattern and do it in proper SwiftUI like this:
struct ContentView: View {
#State private var articles = [Article]()
var body: some View {
NavigationView {
List(articles) { article in
Text(article.title)
}
.navigationTitle("Articles")
}
.task {
do {
let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb")!
let (data, _) = try await URLSession.shared.data(from: url)
articles = try JSONDecoder().decode([Article].self, from: data)
} catch {
articles = []
}
}
}
}

SWIFTUI: Invalid conversion from 'async' function of type '(IndexSet) async

I am learning SWIFTUI and trying to do some app to test my knowledge without actually following a tutorial, as I like to wrap my head around why things work certain way. It's how I learn :)
So, I am trying to call a function to post some data through an API. The method is inside of the ViewModel. The view is a List, and I need to be able to trigger .OnDelete modifier to trigger the API call through the viewmodel.
but I am always faced with the error:
Invalid conversion from 'async' function of type '(IndexSet) async -> Void' to synchronous function type '(IndexSet) -> Void'
here's the code.
in ContentView.swift:
// there is a #State var res = PostsViewModel() above the body!
List {
ForEach(res.posts, id:\.self){ item in
VStack {
Text(item.title)
.font(.headline)
Text("\(item.content) <> \(item._id)")
}
}
.onDelete{ IndexSet in
var index: Int = -1
for idx in IndexSet {
index = idx
}
print("index ix \(index)")
await res.updateData(at: index)
}
}
In PostViewModel.swift:
func updateData(at index: Int) async {
let item = posts[index]
print(item._id)
guard let url = URL(string: "{URL HERE HIDDEN}/api/posts/\(item._id)") else {
print("Error: cannot create URL")
return
}
struct UploadData: Codable {
let title: String
let content: String
}
// Add data to the model
let uploadDataModel = UploadData(title: "Hello", content: "World!!")
// Convert model to JSON data
guard let jsonData = try? JSONEncoder().encode(uploadDataModel) else {
print("Error: Trying to convert model to JSON data")
return
}
// Create the request
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
guard let (data, _) = try? await URLSession.shared.upload(for: request, from: jsonData) else {
print("failed to connect")
return
}
guard let decodedOrder = try JSONDecoder().decode(PostsModel?.self, from: data) else {
print("failed to decode")
return
}
print(decodedOrder.content)
} catch {
print("Checkout failed.")
return
}
}
what am I doing wrong here?
Wrap the asynchronous function in a Task
Task {
await res.updateData(at: index)
}
And if PostsViewModel is a class declare a #StateObject
#StateObject var res = PostsViewModel()

How could you access the GitHub API in Swift?

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

HTTP post request and save response in app

I'm totally new to swift and iOS programming so I'm a little lost on how to do this and even in what files I should be doing this too.
I'm trying to do a http post request to get calendar events and save them in the app to later use and display.
I made a model class with this code.
import UIKit
class Event {
var id: Int
var init_date: String
var end_date: String
var title: String
var description: String
var color_code: String
var all_day: Int
init?(id: Int, init_date: String, end_date: String, title: String, description: String, color_code: String, all_day: Int) {
//Initialization should fail if these are false
if id < 0 || init_date.isEmpty || end_date.isEmpty || title.isEmpty {
return nil
}
//Initialize stored properties
self.id = id
self.init_date = init_date
self.end_date = end_date
self.title = title
self.description = description
self.color_code = color_code
self.all_day = all_day
}
}
But now I don't know what the next step would be. I need this to be downloaded immediately once the app is opened for the first time and not when it's not being opened for the first time. Do I create a new method in the ViewController.swift for the download?
Right now I haven't added anything to the ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
What should I do next?
At this point you need to create a function that handles the POST request you are making.
Once completed, place this function inside your appDelegate main function didFinishLaunchingWithOptions. This is the function that executes on appStart
On a successful function call save the data (presumably json) into a Global Variable or whatever you need for you app.
TIP:
On you class
class Event: Codable {
}
make sure to add Codable like above
Below is an example of what your post request will look like
func myPostRequest(completionHandler: #escaping (Bool?, String?) -> Void){
guard let url = URL(string:"") else { return }
let parameters = ["": ""]
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil
else {
print(error as Any)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200) {
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
//print("^^^^^^^^^^^^^^",json)
for x in json ?? [] {
//here is where you will parse your data from the post request
}
completionHandler(true, nil)
return
}
} else {
completionHandler(false, "No Response From Server")
print("Failure response: STATUS CODE != 200")
}
} else {
completionHandler(false, "Database Connection Error")
print("Error \(error!)")
}
}
task.resume()
} catch let error {
completionHandler(false, "failure")
print("POSTERROR: \(error.localizedDescription)")
}
}
I use Alamofire, you can add it to your project via:
Pods
Swift Package Manager
When you add the framework you can use it:
import Alamofire
Then you need to make your class with the protocol Codable to pass the data to your class.
class Event: Codable { }
Then you need to call the url and store the response in a variable:
override func viewDidAppear(_ animated: Bool) {
AF.request("your API rest url").responseData { (resData) in
guard let data = resData.data else { return }//Check if the data is valid
do {
let decoder = JSONDecoder()//Initialize a Json decoder variable
let decodedData = try decoder.decode(Event.self, from: data)//Decode the response data to your decodable class
//Print the values
print(decodedData.headers)
print(decodedData.id)
print(decodedData.init_date)
print(decodedData.end_date)
} catch {
print(error.localizedDescription)
}
}
}

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