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)
}
Related
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()
}
}
}
I fetched the data and it is showing when printing but when i try to display it on tableview.Nothing is coming
Am i placing tableview.reloadData in wrong place ?
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
tableView.reloadData()
}
func fetchData()
{
if let url = URL(string: urlConstant) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, res, err) in
if err == nil
{
let decoder = JSONDecoder()
if let safeData = data
{
do{
let results = try decoder.decode(Results.self, from: safeData)
guard let array = results.Result as? [Products] else {return }
for product in array
{
self.productArray.append(product)
}
} catch {
print(error)
}
}
}
}
task.resume()
}
self.tableView.reloadData()
}
If you are getting correct response(check it once) from the server then next thing you need to reload tableView after getting the response from the server and populating the array.
func fetchData() {
if let url = URL(string: urlConstant) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, res, err) in
if err == nil
{
let decoder = JSONDecoder()
if let safeData = data
{
do{
let results = try decoder.decode(Results.self, from: safeData)
guard let array = results.Result as? [Products] else {return }
for product in array {
self.productArray.append(product)
}
// Reload table view here
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
Alternative, you can add completion handled in fetchData method.
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
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){
}
I am trying to save "author" data to global variable named "authors" from json(Link:"https://learnappmaking.com/ex/books.json") with these two libraries. But it only works at the trailing closure of func Alamofire.request(url).responseJSON. When I access the global variable named "authors" from somewhere except the trailing closure, what I get is an empty array of string.
Can someone explain the reason behind this werid situation?
Thanks a lot.
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)") // I hope that here, the number of authors should be 3 too! actually, it is 0. Why?
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
}
}
}
getAuthorsCount()
print("-------------")
}
}
the actual output is:
Update:
I adjusted my code:
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)")
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
//print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
//print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount() // I added this line of code.
}
}
}
getAuthorsCount()
print("-------------")
}
}
But why does the func getAuthorsCount() (not self. version) still print an empty array of strings ? I think the result should be the same as the result which
func self.getAuthorsCount() printed.
I am so confused now...
Again, I want to use the data kept in the variable named "authors", but what I only got is an empty array of strings.
I'll try to answer all your questions :
The data is persistant
You are doing the following : Alamo.request (Network call) -> getAuthors(print result - empty) ->
response (receive response) -> self.authors.append(save response) -> self.authors (print result)
You need to do : Alamo.request (Network call) -> response (receive response) -> self.authors.append(save response) -> self.getAuthors or getAuthors(same) (inside the response {})
You need to call getAuthors once you have your result, inside the response callback :
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount()
print("-------------")
//Do whatever you want from here : present/push
}
}
}
Then you can use the saved data :
To send the data to another ViewController you can use various methods (present/push, closure/callback, ...)
Usually you will have a loading spinner to wait for the network to
answer then you will show your next controller
As requested via direct message: a Swift-only approach. Just paste this in a blank Playground:
import Foundation
final class NetworkService {
enum ServiceError: LocalizedError {
case invalidUrl
case networkingError(error: Error)
case parsingError
var localizedDescription: String? { return String(describing: self) }
}
func request(completion: #escaping (Result<[UserObject], Error>) -> Void ) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
completion(.failure(ServiceError.invalidUrl))
return
}
let dataTask = URLSession.shared.dataTask(with: url) { (jsonData, response, error) in
if let jsonData = jsonData {
let jsonDecoder = JSONDecoder()
do {
let users = try jsonDecoder.decode([UserObject].self, from: jsonData)
completion(.success(users))
} catch {
completion(.failure(ServiceError.parsingError))
}
} else if let error = error {
completion(.failure(ServiceError.networkingError(error: error)))
}
}
dataTask.resume()
}
}
struct UserObject: Codable {
let id: Int
let name: String
let username: String
let email: String?
let website: String?
}
let networkService = NetworkService()
networkService.request { result in
switch result {
case .success(let users):
debugPrint("Received \(users.count) users from REST API")
debugPrint(users)
case .failure(let error):
debugPrint(error.localizedDescription)
}
}