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)
}
}
}
Related
I'm creating a basic application in swift where I get data from an api and show it. My Model class handles the fetching data part and I use a delegate to communicate data with the ViewController.
However when fetched data is passed into the controller it is nill for some reason.
Here are the things I know/have tried.
Extracting the JSON is working. The data is correctly fetched.
I tried using viewWillAppear as well as viewDidLoad
I have set the delegate to self in the View Controller
Abstracted code is below.
//Model Class
protocol DataDetailDelegate {
func datadetailFetched(_ datadetail: DataDetailItem)
}
class Model {
var datadetaildelegate: DataDetailDelegate?
func fetchData(ID: String) {
let url = URL(string: Constants.Data_URL + ID)
guard url != nil else {
print("Invalid URL!")
return
}
let session = URLSession.shared.dataTask(with: url!) { data, response, error in
if error != nil && data == nil {
print("There was a data retriving error!")
return
}
let decoder = JSONDecoder()
do {
let response = try decoder.decode(MealDetail.self, from: data!)
if response != nil {
DispatchQueue.main.async {
self.datadetaildelegate?.datadetailFetched(response)
}
}
} catch {
print(error.localizedDescription)
}
}
session.resume()
}
}
//View Controller
import UIKit
class DetailViewController: UIViewController, DataDetailDelegate {
#IBOutlet weak var instruction: UITextView!
var model = Model()
var datadetail : DataDetailItem?
override func viewDidLoad() {
super.viewDidLoad()
model.datadetaildelegate = self
model.fetchData(ID: data.id)
if self.datadetail == nil {
print("No data detail available")
return
}
self.instruction.text = datadetail.instruction
}
func datadetailFetched(_ datadetail: DataDetailItem) {
self.datadetail = datadetail
}
}
As mentioned in the comments, there are many problems with the above code, but the main one being not recognising the asynchronous nature of the fetchData.
The below is a rough solution that addresses the critical issues with your code, rather than being best practice and addressing all of them. For example you could also check the contents of error and response codes, and pass them back through the completion handler (maybe as a Result type).
For starters, streamline your fetchData so that it gets the data and then handles that data via a completion handler in an asynchronous manner. Also, don't call it a Model as it's really not.
class SessionManager {
func fetchData(withID id: String, completion: #escaping (data) -> Void) {
guard let url = URL(string: Constants.Data_URL + ID) else {
print("Invalid URL!")
return
}
let session = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil, let data = data else { //could be done far better by checking error and response codes
print("There was a data retriving error!")
return
}
completion(data)
}
session.resume()
}
Then adapt your view conroller so that it creates the session manager and fetches the data, but processes it in the completion handler. I've only added the relevant content.
class DetailViewController: UIViewController, DataDetailDelegate {
lazy var sessionManager = SessionManager()
var datadetail : DataDetailItem?
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.fetchData(withID: data.id){ [weak self] data in
let decoder = JSONDecoder()
do {
let response = try decoder.decode(MealDetail.self, from: data)
DispatchQueue.main.async {
self?.datadetail = response
self?.instruction.text = datadetail.instruction
}
} catch {
print(error.localizedDescription)
}
}
}
// rest of view controller
}
That should be enough to get you going. There are many best practice examples/tutorials scattered around that would be worth a look so you can refine things further.
Note: this has been written in here without a compiler to hand, so there may be some syntax that needs tweaking.
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
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)
}
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("🌈🌈🌈🌈🌈🌈🌈In closure function to update articles🌈🌈🌈🌈🌈🌈🌈")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
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)
}
}