Two days already of learning how to make my own network layer to fetching data from server using API and json decoder
But after two days of many lessons online I can't find what's after creating the network layer in Swift 5,
I'm getting json data in output by print(data) in serviceLayer Class file but can't print the data in SwiftUI List View?
here's the serviceLayer.swift file
import Foundation
class ServiceLayer {
// 1.
class func request<T: Decodable>(router: Router, completion: #escaping (Result<[String: [T]], Error>) -> ()) {
// 2.
var components = URLComponents()
components.scheme = router.scheme
components.host = router.host
components.path = router.path
components.queryItems = router.parameters
// 3.
guard let url = components.url else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = router.method
// 4.
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: urlRequest) { data, response, error in
// 5.
guard error == nil else {
completion(.failure(error!))
print(error?.localizedDescription)
return
}
guard response != nil else {
return
}
guard let data = data else {
return
}
// 6.
let responseObject = try! JSONDecoder().decode([String: [T]].self, from: data)
// 7.
DispatchQueue.main.async {
// 8.
completion(.success(responseObject))
}
}
dataTask.resume()
}
}
And here is the model I'm using
struct CollectionItem: Decodable {
let title: String
let id: Int
}
In the swift ui define a property with #State property wrapper, assign the responseObject to this property. SwiftUI automatically reloads when the #State property is set.
If you are willing to use Combine, https://dev.to/kevinmaarek/getting-started-with-swiftui-and-combine-dd8
Related
I was trying to learn the basics of networking with openweather api.
Implemented a very basic struct like this.
protocol WeatherManagerDelegate {
func didUpdateWeather(_ weatherManager : WeatherManager, weather : WeatherModel)
func didFailWithError(error: Error)
}
struct WeatherManager {
var delegate : WeatherManagerDelegate?
var temp : Double = 0.0
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=40ca58efce193db0fc801564afb08283&units=metric"
func fetchWheather(cityName : String){
let urlString = "\(weatherURL)&q=\(cityName)"
performRequest(with: urlString)
}
func performRequest(with urlString: String){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url){ (data, response, error) in
if error != nil{
delegate?.didFailWithError(error: error!)
return
}
if let safedata = data {
if let weather = self.parseJSON(weatherData: safedata){
// let WeatherVC = WeatherViewController()
self.delegate!.didUpdateWeather(self, weather: weather)
}
print("Data is \(safedata)")
}
}
task.resume()
}
}
func parseJSON(weatherData:Data)-> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
let id = decodedData.weather[0].id
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionID: id, cityName: name, temperature: temp)
return weather
} catch{
print("error is \(error)")
delegate?.didFailWithError(error: error)
return nil
}
}
}
But the issue is none of the print statements inside the requests are giving any outputs , nor i am able to update the UI. But the URL is giving the correct JSON Response when tried in browser
I am using the latest XCode on iOS 15 (iPhone 11 Device) with M1 Pro chip mac.
Found some threads which mentions to use "open with rosetta" but none of which worked.
also, not getting any errors on the console
Any solution?
Edit : Called in VC like this:
func textFieldDidEndEditing(_ textField: UITextField) {
if let city = searchTextField.text {
weatherManager.fetchWeather(cityName: city)
}
searchTextField.text = ""
}
Please try using:
let session = URLSession.shared
…instead of creating local session variable within the function scope
And if error != nil then don't return from that block. Simply use if-else for error handling.
I'm very new in programing, I have been learning swift5 for a few months now. I'm trying to make an APP that scan a 1D or 2D barcode, take the string and send it to an API endpoint which will give me back some info, for example, product number and batch number.
For the barcode part I'm using a really nice software from GitHub called BarcodeScanner. It is running without any issue and I'm able to get the string from the barcode it reads.
Once I have the string I'm able to send it to the API endpoint but then it gets stuck there.
If I run the same code in a playground it works without problems, it makes the request and I can see the answer in JSON from the server. If I run the code on the iPhone then it gets stuck. Of course I cannot use the scanning part when in playground, I use it to test the networking part and communication with the API.
Here is my Networking.swift file, it has some print() used for debug:
import Foundation
import UIKit
protocol NetworkManagerDelegate: class {
func didUpdatePharmo(pharmo: PharmoModel)
func didUpdateBarcode(barcode: BarcodeModel)
}
public struct NetworkManager {
var delegate: NetworkManagerDelegate?
let pharmoURL = "https://someurl.com/api"
let pharmoEndpoint = "https://someendpoint.com/api"
func fetchData(productNumber: String) {
let urlString = "\(pharmoURL)&number=\(productNumber)"
performRequestProductInfo(urlString: urlString)
print(urlString)
}
func fetchEndpoint(dataMatrix: String) {
let urlString = "\(pharmoEndpoint)&datamatrix=\(dataMatrix)"
performRequestEndpoint(urlString: urlString)
print(urlString)
}
func performRequestEndpoint(urlString: String) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let response = response as? HTTPURLResponse {
print("EndPoint HTTP Status code: \(response.statusCode)")
}
if let safeData = data {
if let barcode = self.parseJSONEndpoint(endPointData: safeData) {
self.delegate?.didUpdateBarcode(barcode: barcode)
}
}
}
task.resume()
}
}
func parseJSONEndpoint(endPointData: Data) -> BarcodeModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(BarcodeData.self, from: endPointData)
let productNumber = decodedData.product_number
let batchNumber = decodedData.batchnumber
let barcode = BarcodeModel(productNumber: productNumber, batchNumber: batchNumber)
print(batchNumber)
return barcode
} catch {
print(error)
return nil
}
}
func performRequestProductInfo(urlString: String) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let response = response as? HTTPURLResponse {
print("ProductInfo HTTP Status code: \(response.statusCode)")
}
if let safeData = data {
if let pharmo = self.parseJSONProductInfo(pharmoData: safeData) {
self.delegate?.didUpdatePharmo(pharmo: pharmo)
}
}
}
task.resume()
}
}
func parseJSONProductInfo(pharmoData: Data) -> PharmoModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(PharmoData.self, from: pharmoData)
let id = decodedData.id
let number = decodedData.number
let name = decodedData.name
let atcCode = decodedData.atc_code
let imagePath = decodedData.image_path
let displayImage = decodedData.display_image
let storageGroupId = decodedData.storage_group_id
let animalGroupId = decodedData.animal_group_id
let udlevbest = decodedData.udlevbest
let packageSizeText = decodedData.package_size_text
let packageSizeNum = decodedData.package_size_num
let unitCode = decodedData.unit_code
let strengthText = decodedData.strength_text
let prodPrice = decodedData.prod_price
let aupPrice = decodedData.aup_price
let aupInstPrice = decodedData.aup_inst_price
let animalGroups = decodedData.animal_groups
let udleveringsbestemmelse = decodedData.udleveringsbestemmelse
let animalGroup = decodedData.animal_group
let storageGroup = decodedData.storage_group
let species = decodedData.species
let indications = decodedData.indications
let substances = decodedData.substances
let manufacturer = decodedData.manufacturer
let pharmo = PharmoModel(id: id, number: number, name: name, atc_code: atcCode, image_path: imagePath, display_image: displayImage, storage_group_id: storageGroupId, animal_group_id: animalGroupId, udlevbest: udlevbest, package_size_text: packageSizeText, package_size_num: packageSizeNum, unit_code: unitCode, strength_text: strengthText, prod_price: prodPrice, aup_price: aupPrice, aup_inst_price: aupInstPrice, animal_groups: animalGroups, udleveringsbestemmelse: udleveringsbestemmelse, animal_group: animalGroup, storage_group: storageGroup, species: species, indications: indications, substances: substances, manufacturer: manufacturer)
print(name)
return pharmo
} catch {
print(error)
return nil
}
}
}
From the ViewController.swift running the barcode scanner I call the function fetchEndpoint(dataMatrix: String)
extension ViewController: BarcodeScannerCodeDelegate {
func scanner(_ controller: BarcodeScannerViewController, didCaptureCode code: String, type: String) {
print("Barcode Data: \(code)")
print("Symbology Type: \(type)")
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
networkManager.fetchEndpoint(dataMatrix: code)
}
}
}
From the console output I can see this 2 print() statements from ViewController.swift:
print("Barcode Data: \(code)")
print("Symbology Type: \(type)")
I can also see this print() statement from Networking.swift:
print(urlString)
But nothing happends after that. Again if I run the same code in a playground it works fine. I have also made requests using Postman, so I can see the JSON file and be sure the urlstring is working.
Anyone have some idea what am I doing wrong?
I was able to find the problem. The string being read from the scanner had an non ASCII character.
At any point XCode said anything about a non ASCII character, I had to copy the string into a playground, then I received this error:
Unprintable ASCII character found in source file
I copied the string into a terminal shell and I was able to see the character.
datamatrix=^]01036611030467831722113010RXT0711GC
I will try to use URLComponents or percent encoding
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I'm coming from a JavaScript/Flutter environment and I need to await an asynchronous function in Swift. Is there a way to do it?
Basically, I want to return repoNames from a class but a "task" is executing way faster than a return keyword.
Here's the code:
import UIKit
class SearchViewModel {
let textFieldText: String?
var repoNames: Array<String> = []
init(textFieldText: String?) {
self.textFieldText = textFieldText
}
func searchForRepo() -> Array<String> {
let baseUrl = "https://api.github.com/search/repositories?q="
let finalUrl = URL(string: baseUrl + self.textFieldText!)
let session = URLSession.shared
let task = session.dataTask(with: finalUrl!) { (data, response, error) in
guard let data = data else { return }
let repos: Items = try! JSONDecoder().decode(Items.self, from: data)
for repo in repos.items {
self.repoNames.append(repo.name)
print(self.repoNames)
}
}
task.resume()
return self.repoNames
}
}
Add a completion
func searchForRepo(completion:#escaping( [String] -> ())) -> {
let baseUrl = "https://api.github.com/search/repositories?q="
let finalUrl = URL(string: baseUrl + self.textFieldText!)
URLSession.shared.dataTask(with: finalUrl!) { (data, response, error) in
guard let data = data else { return }
do {
let repos = try JSONDecoder().decode(Items.self, from: data)
self.repoNames = repos.items.map { $0repo.name }
completion(self.repoNames)
}
catch {
print(error)
}
}.resume()
}
When calling
SearchViewModel(textFieldText:"someValue").searchForRepo { res in
print(res)
}
I'm trying to decode data from this massive JSON Array https://coronavirus-19-api.herokuapp.com/countries I've had luck decoding by country or using the total stat worldwide https://coronavirus-19-api.herokuapp.com/all
by doing the following
//
// GlobalSwiftViewController.swift
// Universal
//
// Created by JOE on 3/20/20.
import UIKit
final class StatSwiftViewController: UIViewController {
// THESE LABELS WILL RETRIEVE THE FOLLOWING DATA FROM THE URL: THE CASE , DEATH AND RECOVERED DATA
#IBOutlet weak var CaseLable: UILabel!
#IBOutlet weak var DeathLable: UILabel!
#IBOutlet weak var RecoveredLabel: UILabel!
struct JSONTest: Decodable {
let cases: Double
let deaths: Float
let recovered: Int?
}
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://coronavirus-19-api.herokuapp.com/all"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}
guard let data = data else { return }
do {
//Decode data
let urlString = try JSONDecoder().decode(JSONTest.self, from: data)
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
//HERE WE ARE SHOWING TO THE USER THE DATA FROM THE URL ABOVE
DispatchQueue.main.async {
self.CaseLable.text = numberFormatter.string(for: urlString.cases)
self.DeathLable.text = numberFormatter.string(for: urlString.deaths)
self.RecoveredLabel.text = numberFormatter.string(for: urlString.recovered)
//self.timeLabel.text = JSONTest.deaths
}
} catch let jsonError {
print(jsonError)
}
}.resume()
}
}
Now I'm trying to decode all of the data in this URL https://coronavirus-19-api.herokuapp.com/countries to show in one view controller, I've had success by using the single URL https://coronavirus-19-api.herokuapp.com/countries/china for the country using the same code above by just adding more vars and labels However, I'm not able to add more counties by adding each URL for each country or using the main URL for all countries https://coronavirus-19-api.herokuapp.com/countries Therefore, How can I struct all Array List using the URL for all countries?
note: Im trying to edit/update my code above to get the results as possible without installing extra pods or files...
Try to adapt your model to be able to decode the countries data.
You can test this in a Playground:
import Foundation
struct JSONTestElement: Codable {
let country: String
let cases, todayCases, deaths, todayDeaths: Int
let recovered, active, critical, casesPerOneMillion: Int
}
typealias JSONTest = [JSONTestElement]
func decode() {
let urlString = "https://coronavirus-19-api.herokuapp.com/countries"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}
guard let data = data else { return }
do {
//Decode data
let countriesData = try JSONDecoder().decode(JSONTest.self, from: data)
let china = countriesData.filter({ $0.country.contains("China")})
print("China data: \(china)")
} catch let jsonError {
print(jsonError)
}
}.resume()
}
decode()
I'm trying to get some data from the server and use it globally in the app..
I mean for example, I'm using following code to get data from service:
struct Service : Decodable{
let id: Int
let name, description: String
let createdAt: String?
let updatedAt: String?
}
func makeGetCall() {
let todoEndpoint: String = "http://web.src01.view.beta.is.sa/public/api/services"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let todos = try decoder.decode([Service].self, from: responseData)
for todo in todos{
print(todo.name)
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
This code is located and called in HomeViewController and i'm getting data which i want.
But i want to access and use this data in another viewcontroller and in whole app...
How i can do it? How can i make the received data from the function is saved globally and how to use it in another viewcontroller?
Can someone tell me how i can do this?
For such cases we usually use static data. They may be served as singleton or just a static property. In your case a static property for cached data may be nice. We can put static properties in extension so adding following may be nice:
// MARK: - Fetching Data
extension Service {
private static var cachedServices: [Service]?
static func fetchServices(_ completion: (_ services: [Service]) -> Void?) {
if let cachedServices = cachedServices {
completion(cachedServices)
} else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
}
Now the usage from everywhere is calling
Service.fetchServices { services in
}
and this call may be asynchronous or not, depending if data is already loaded.
If you need to access them synchronous and you are sure data is already loaded then simply add another method in extension:
static func getCachedData() -> [Service] {
return cachedServices ?? []
}
This method will return instantly but array will be empty if no data was received yet. But anywhere you can call Service.getCachedData()
This cache is now only preserved until your app terminates. If you want to preserve them longer then all you need to do is add the logic to save and load data into file or user defaults. The logic for that would be something like:
private static var cachedServices: [Service]? {
didSet {
self.saveServicesToFile(cachedServices)
}
}
static func fetchServices(_ completion: (_ services: [Service]) -> Void?)
{
if let cachedServices = cachedServices {
completion(cachedServices)
} else if let saved = self.loadFromFile() {
self.cachedServices = saved
completion(saved)
}else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}