How do I get multiple JSON objects from api call? - ios

I am trying to make an API call to the GitLab API to get the projects that are available to a particular user.
I can get one project of an index of my choosing, put it into a ProjectModel with the projectId and the projectName but I can not figure out how to get all of them into an array of ProjectModels.
By printing then I can see them all being printed in the console but it will not let me append them to an array.
It is in the parseJSON function that I am trying to get a hold of all of the projects.
Does anyone have any suggestions?
This is my manager to fetch the projects:
protocol FetchProjectsManagerDelegate {
func didUpdateProjects(_ fetchProjectsManager: FetchProjectsManager, project: ProjectModel?)
func didFailWithError(error: Error)
}
struct FetchProjectsManager {
let projectsURL = "secret"
var delegate: FetchProjectsManagerDelegate?
func fetchProjects(privateToken: String) {
let privateTokenString = "\(projectsURL)projects?private_token=\(privateToken)"
performRequest(with: privateTokenString)
}
func performRequest(with privateTokenString: String) {
// Create url
if let url = URL(string: privateTokenString) {
// Create URLSession
let session = URLSession(configuration: .default)
// Give the session a task
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let project = self.parseJSON(safeData) {
self.delegate?.didUpdateProjects(self, project: project)
}
}
}
// Start the task
task.resume()
}
}
func parseJSON(_ projectData: Data) -> ProjectModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode([Project].self, from: projectData)
for project in decodedData {
print(project)
}
let projectId = decodedData[0].id
let projectName = decodedData[0].name
let project = ProjectModel(projectId: projectId, projectName: projectName)
return project
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
}
This is my project model
struct ProjectModel {
let projectId: Int
let projectName: String
}

Your parseJson method only returns a single project instead of all of them, change it to
func parseJSON(_ projectData: Data) -> [ProjectModel]? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode([Project].self, from: projectData)
let projects = decodedData.map { ProjectModel(projectId: $0.id,
projectName: $0.name) }
return projects
} catch {
delegate?.didFailWithError(error: error)
return nil
}
}
and you of course need to update didUpdateProjects so that it takes an array of ProjectModel or call it in a loop

Related

DispatchGroup Order of Operation

Hello i am trying to get result based on a result from another api.The getLatest function is going to get the latest object and will return the number then i want to generate a list of urls with the latest number and then sequentially get the data in the function getXKCDData.How can i achieve this.The dispatchgroup i have on there works sometimes but doesnt work all the time because generateUrlList is called before getting the latest number and it generate the wrong url.Which causes The operation couldn’t be completed. (XKCD_Comics.NetworkError error 1.) error.How can i achieve this.
final class NetworkManager {
public static var shared = NetworkManager()
private let sessions = URLSession.shared
private func generateUrlList(latestXKCD: Int) -> [String] {
print("Here at generateUrlList")
var urls = [String]()
for i in latestXKCD-100...latestXKCD{
let xkcd_url = "https://xkcd.com/\(i)/info.0.json"
urls.append(xkcd_url)
}
return urls
}
private func getLatest(completion: #escaping(Int) -> ()){
print("Here at get latest")
guard let url = URL(string: "https://xkcd.com/info.0.json") else { return }
let task = sessions.dataTask(with: url) { data, response, error in
guard let jsonData = data else { return }
do {
let decoder = JSONDecoder()
let response = try decoder.decode(Comics.self, from: jsonData)
completion(response.num)
}catch {
completion(0)
}
}
task.resume()
}
public func getXKCDData(completion: #escaping(Result<Comics,NetworkError>) -> Void){
let group = DispatchGroup()
var latestID = 0
group.enter()
self.getLatest { (result) in
latestID = result
group.leave()
}
let urlList = generateUrlList(latestXKCD: latestID)
print(urlList)
print("Here at getXKCDData")
for url in urlList {
group.enter()
guard let url = URL(string: url) else {
completion(.failure(.InvalidURL))
return
}
let task = sessions.dataTask(with: url) { data, response, error in
guard let jsonData = data else {
completion(.failure(.NoDataAvailable))
return
}
do {
let decoder = JSONDecoder()
let response = try decoder.decode(Comics.self, from: jsonData)
completion(.success(response))
group.leave()
}catch {
completion(.failure(.CanNotProcessData))
}
}
task.resume()
}
}
}

How to await an asynchronous function in Swift? [duplicate]

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

How to get an array from URLSession

Trying to make a program for a news site. I take information from the site through the api, everything works fine.
The only question is, how do I get this array out of the loop?
Here is my code:
import UIKit
class ViewController: UIViewController {
var news:[News] = []
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
func getUsers() {
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
news = try JSONDecoder().decode([News].self, from: data)
// print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
}
struct News:Codable, CustomStringConvertible{
let href:String?
let site:String?
let title:String?
let time:String?
var description: String {
return "(href:- \(href), site:- \(site), title:- \(title), time:- \(time))"
}
}
Declare news array in your class and assign the response to this array in getUsers method
var news:[News] = []
func getUsers(){
guard let url = URL(string: "https") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
self.news = try JSONDecoder().decode([News].self, from: data)
print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
The fundamental problem is you are retrieving data asynchronously (e.g. getUsers will initiate a relatively slow request from the network using URLSession, but returns immediately). Thus this won’t work:
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
You are returning from getUsers before the news has been retrieved. So news will still be [].
The solution is to give getUsers a “completion handler”, a parameter where you can specify what code should be performed when the asynchronous request is done:
enum NewsError: Error {
case invalidURL
case invalidResponse(URLResponse?)
}
func getUsers(completion: #escaping (Result<[News], Error>) -> Void) {
let queue = DispatchQueue.main
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {
queue.async { completion(.failure(NewsError.invalidURL)) }
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
queue.async { completion(.failure(error)) }
return
}
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
queue.async { completion(.failure(NewsError.invalidResponse(response))) }
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let news = try decoder.decode([News].self, from: data)
queue.async { completion(.success(news)) }
} catch let parseError {
queue.async { completion(.failure(parseError)) }
}
}.resume()
}
Then your view controller can fetch the news, passing a “closure”, i.e. code that says what to do when the asynchronous call is complete. In this case, it will set self.news and trigger the necessary UI update (e.g. maybe refresh tableview):
class ViewController: UIViewController {
var news: [News] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchNews()
}
func fetchNews() {
getUsers() { result in
switch result {
case .failure(let error):
print(error)
case .success(let news):
self.news = news
print(news)
}
// trigger whatever UI update you want here, e.g., if using a table view:
//
// self.tableView.reloadData()
}
// but don't try to print the news here, as it hasn't been retrieved yet
// print(news)
}

UITextField to change API URL in Swift 5

I am new iOS Developer
I want to change the websiteLogo API with a textfield to change the URL.
how can I change the line with the ***
with a var and a textfield in my viewcontroller?
With screenshoot it's will be easier to understand what I want? Thank you !!! Guys. OneDriveLink. 1drv.ms/u/s!AsBvdkER6lq7klAqQMW9jOWQkzfl?e=fyqOeN
private init() {}
**private static var pictureUrl = URL(string: "https://logo.clearbit.com/:http://www.rds.ca")!**
private var task: URLSessionDataTask?
func getQuote(callback: #escaping (Bool, imageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: QuoteService.pictureUrl) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = imageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
First, please don't use screenshots do show your code. If you want help, others typically copy/paste your code to check whats wrong with it.
There are some minor issues with your code. Some hints from me:
Start your types with a big letter, like ImageLogo not imageLogo:
Avoid statics
Avoid singletons (they are almost statics)
Hand in the pictureUrl into getQuote
struct ImageLogo {
var image:Data
}
class QuoteService {
private var task: URLSessionDataTask?
func getQuote(from pictureUrl:URL, callback: #escaping (Bool, ImageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: pictureUrl) {
(data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = ImageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
}
Store an instance of QuoteService in your view controller
Call getQuote on that instance, handing in the pictureUrl
class ViewController : UIViewController {
var quoteService:QuoteService!
override func viewDidLoad() {
self.quoteService = QuoteService()
}
func toggleActivityIndicator(shown:Bool) { /* ... */ }
func update(quote:ImageLogo) { /* ... */ }
func presentAlert() { /* ... */ }
func updateconcept() {
guard let url = URL(string:textField.text!) else {
print ("invalid url")
return
}
toggleActivityIndicator(shown:true)
quoteService.getQuote(from:url) {
(success, quote) in
self.toggleActivityIndicator(shown:false)
if success, let quote = quote {
self.update(quote:quote)
} else {
self.presentAlert()
}
}
}
/* ... */
}
Hope it helps.
I think you want to pass textfield Text(URL Enter By user) in Web Services
Add a parameter url_str in getQuote function definition first and pass textfield value on that parameters
fun getQuote(url_str : String, callback : #escaping(Bool, ImgaeLogo/)->void){
}

Globally save and access received data

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

Resources