I am making an application which makes a lot of requests from an API. So I don't want to copy and past the code over and over. I was wondering how I can reuse my code in a some more efficient way? Maybe with extensions?
This is my code know:
func apiRequest() {
let config = URLSessionConfiguration.default
let username = "****"
let password = "****"
let loginString = String(format: "%#:%#", username, password)
let userPasswordData = loginString.data(using: String.Encoding.utf8)
let base64EncodedCredential = userPasswordData?.base64EncodedString()
let authString = "Basic " + (base64EncodedCredential)!
print(authString)
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
var running = false
let urlProjects = NSURL(string: "https://start.jamespro.nl/v4/api/json/projects/?limit=10")
let task = session.dataTask(with: urlProjects! as URL) {
( data, response, error) in
if let taskHeader = response as? HTTPURLResponse {
print(taskHeader.statusCode)
}
if error != nil {
print("There is an error!!!")
print(error)
} else {
if let content = data {
do {
let dictionary = try JSONSerialization.jsonObject(with: content) as! [String:Any]
print(dictionary)
if let items = dictionary["items"] as? [[String:Any]] {
for item in items {
if let description = item["Description"] as? String {
self.projectNaam.append(description)
}
if let id = item["Id"] as? String {
self.projectId.append(id)
}
if let companyId = item["CompanyId"] as? String {
self.companyId.append(companyId)
}
}
}
self.apiRequestCompani()
}
catch {
print("Error: Could not get any data")
}
}
}
running = false
}
running = true
task.resume()
while running {
print("waiting...")
sleep(1)
}
}
Yes, you can use Extensions to create a BaseViewController and extend that where you want to use your code over and over again. Then you should abstract all dynamic data over input parameters to that method.
import UIKit
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func getApiRequest (Parameters) {
//API Request
}
And then in your view controller you just extend BaseViewController
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Call method in baseviewcontroller
getApiRequest(parameters)
//Call method in self
self.getApiRequest(parameters)
}
override func getApiRequest(Parameters) {
//IF you need to override default configuration
}
So I don't want to copy and past the code over and over.
Absolutely right, no one aiming to get duplicated code; That's the issue of massive view controller. This issue appears since the view controller layer in your application handles most of the responsibilities, such as: getting data from the network, how data should be represented, deliver the formatted data to the view layer, etc...
There are many approaches for solving such an issue (using an appropriate architectural pattern for your application), for simplicity, I would recommend to apply the MVC-N (or MVCNetworking) approach into your app, it is almost the same usual MVC, with a separated files (managers), represent a new layer for handling -for instance- the integration with the external APIs.
Applying the MVN-N should not be that complex, nevertheless it needs to be described well (which might be too abroad to be descried in the answer), I would suggest to check the above mentioned apple example, also watching this video should be useful.
Related
The code in my viewcontroller-class is executed before the JSON-download-process is ready even though there is a completion handler in the func for downloading JSON an a DispatchGroup(). I store the JSON-data in an array called "fetchedModules" and this is filled with 11 items in this case. Why does this happen?
result in console:
---> in Class PostCell - func numberOfSections: 0
JSON call finished
// ViewController
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
group.enter()
self.fetchJSON()
// here calling downloadJSONasync
group.leave()
group.notify(queue: .main) {
print("JSON call finished")
}
...
// networkService with completion
func downloadJSONasync(searchItem: String, completion: #escaping ([NSDictionary]) -> Void) {
//request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
//let session = URLSession(configuration: configuration, delegate: nil)
let session = URLSession(configuration: configuration)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
if (error != nil) {
print("error!")
}
else{
do {
let fetchedData = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [NSDictionary]
completion(fetchedData)
}
catch {
print("error")
}
}
})
task.resume()
}
// call in viewController
override func numberOfSections(in tableView: UITableView) -> Int {
print("---> in Class PostCell - func numberOfSections: \(String(describing: fetchedModules.count))")
return fetchedModules.count
// code of fetchJSON
func fetchJSON()
{
let baseurl = AppConstants.Domains.baseurl // https://m.myapp2go.de
let compositURL = baseurl + "getmodules_noItems.php?id=\(AppConstants.appString.startString)"
let encodedUrl : String! = compositURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) // remove the spaces in the url string for safty reason
let JSONurl = URL(string: encodedUrl)! // convert the string into url
var JSONrequest = URLRequest(url: JSONurl) // make request
JSONrequest.httpMethod = "GET"
//JSONrequest.cachePolicy = .reloadIgnoringCacheData
let networkService = NetworkService(request: JSONrequest)
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString, completion: { (fetchedData) in
fetchedModules.removeAll()
DispatchQueue.main.async {
for eachFetchedModul in fetchedData {
let eachModul = eachFetchedModul
if
let custid = eachModul["custid"] as? String,
let modulcat = eachModul["modulcat"] as? String,
let modulname = eachModul["modulname"] as? String,
let comment = eachModul["comment"] as? String
{
fetchedModules.append(CModules(custid: custid, modulcat: modulcat, modulname: modulname, comment: comment))
print(custid)
print(modulcat)
print(modulname)
print(comment)
print("---------------------")
}
}// for end
// ... somehow set data source array for your table view
self.tableView.reloadData()
}// dispatch
}
)} // func end
Because fetchJSON returns immediately, before the JSON is downloaded. The effect is that the DispatchGroup is entereed and left right away, without waiting for the JSON:
group.enter()
self.fetchJSON() // returns immediately
group.leave() // the JSON has yet to be downloaded
To wait until the JSON has arrived, add a completion handler to fetchJSON:
override func viewDidLoad() {
group.enter()
self.fetchJSON {
group.notify(queue: .main) {
print("JSON call finished")
}
group.leave()
}
}
// Change the data type of the completion handler accordingly
func fetchJSON(completionHandler: #escaping (Data?) -> Void) {
// ...
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString) { fetchedData in
defer { completionHandler(fetchedData) }
// ...
}
)
Using defer ensures that the completion handler will always be called, no matter how the outer closure returns. I'm not clear why you use a DispatchGroup here as there is no waiting, but I kept it in place to answer your question.
Your table view doesn't have any data from the beginning because any data hasn't been fetched yet. So it's ok, that table view has no cells. You just need to reloadData of your table view since now you appended elements to table view's data source array and now you should show this data.
Please, don't use DispatchGroup for this, just use your completion parameter of your method and inside of the completion closure after data are received set data source array for table view and then ... reload data of table view
downloadJSONasync(searchItem: "someString") { dictionary in
DispatchQueue.main.async { // don't forget that your code doesn't run on the main thread
// ... somehow set data source array for your table view
self.tableView.reloadData()
}
}
Note that you should avoid using NSDictonary and you should rather use Dictionary. Also from Swift 4+ you can use Codable instead of JSONSerialization.
I am using a tabbarcontroller to show 3 xib's. I would like to decode JSON data in the UITabBarController subclass, and then share the data with the view controllers (as I understand that is the preferred way to do this). I had already successfully accomplished this individually in each view controller, where the same JSON data was getting decoded separately 3 times, but I am now trying to make the process more efficient by only dealing with JSON once.
I am currently getting the following error
"Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee7ab7d98)".
Below is the code I am currently using. I'm mostly only including the code for the first view controller, but it is the same for the others
Here is one of the view controllers. Any help would be appreciated, thank you!
class FirstCollectionViewController: UIViewController {
var tbvc = CustomTabBar()
var statisticsData = [Model]()
let firstCellIdentifier = "FirstCellIdentifier"
#IBOutlet weak var FirstCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
FirstCollectionView.delegate = self
FirstCollectionView.dataSource = self
FirstCollectionView.register(UINib(nibName: "FirstCollectionViewCell", bundle: nil),forCellWithReuseIdentifier: firstCellIdentifier)
}
}
Here is the subclasses UITabBarController
import UIKit
class CustomTabBar: UITabBarController {
let website = "https:......."
var statisticsData = [Model]()
override func viewDidLoad() {
super.viewDidLoad()
let firstTab = FirstCollectionViewController(nibName: "FirstCollectionViewController", bundle: nil)
let secondTab = SecondCollectionViewController(nibName: "SecondCollectionViewController", bundle: nil)
let thirdTab = ThirdCollectionViewController(nibName: "ThirdCollectionViewController", bundle: nil)
viewControllers = [firstTab, secondTab, thirdTab]
downloadJSON(website: website) {
firstTab.statisticsData = self.statisticsData
secondTab.statisticsData = self.statisticsData
thirdTab.statisticsData = self.statisticsData
firstTab.FirstCollectionView.reloadData()
secondTab.SecondCollectionView.reloadData()
thirdTab.ThirdCollectionView.reloadData()
}
}
func downloadJSON(website:String, completed:#escaping ()->()){
guard let qurl = URL(string: website) else { return }
URLSession.shared.dataTask(with: qurl) { (data, response, error) in
if error == nil {
do{
self.statisticsData = try JSONDecoder().decode([Model].self, from: data!)
DispatchQueue.main.async{
completed()
}
} catch {
print("JSON Error")
}}
}.resume()
}
}
Once the data is loaded, you should assign the data to the viewControllers that are added in the tabBarController's Child list as below,
downloadJSON(website: website) {
firstTab.statisticsData = self.statisticsData
secondTab.statisticsData = self.statisticsData
thirdTab.statisticsData = self.statisticsData
firstTab.FirstCollectionView.reloadData()
secondTab.SecondCollectionView.reloadData()
thirdTab.ThirdCollectionView.reloadData()
}
You can also remove the below lines from viewDidLoad of FirstCollectionViewController, SecondCollectionViewController and ThirdCollectionViewController
tbvc = tabBarController as! CustomTabBar
statisticsData = tbvc.statisticsData
I'm a beginner at Swift so let me know if this doesn't quite make sense, but i have a JSON file that i can access in swift and parse into an array, from there i can get a string from the array and store it in a var. I want to be able to access this variable globally but i'm not sure how to do it.
With the help of another user "rmaddy". I have this code:
struct Games: Decodable {
let videoLink: String
}
class BroadService {
static let sharedInstance = BroadService()
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
let jsonUrlString = "LINK IS HERE."
guard let url = URL(string: jsonUrlString) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
}
}
I can then access it in another class from here:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
}
I want to be able to access the contents of "videoLink" globally, without having to use "BroadService.sharedInstance.fetchBroadcasts { (games) in" how would i go about doing this
You shouldn't use global variables, I don't think that's recommended in any language.
Now here you have what looks like a Singleton class (BroadService), that's good because it's a nice solution for what you're looking for.
Next all you need to do is add a property to that class. Let's say videoLink is a string, you can add a string property to BroadService, for example storedVideoLink as an optional String, and the next time you need to obtain that value after you have already fetched it, you can access it like so: BroadService.sharedInstance.storedVideoLink.
One more thing, to have BroadService work properly as a singleton, you should make its init private.
To sum up, here's what I'm suggesting:
class BroadService {
static let sharedInstance = BroadService()
var storedVideoLink: String?
private init() {} // to ensure only this class can init itself
func fetchBroadcasts(completion: #escaping ([Games]?) -> ()) {
// your code here
}
}
// somewhere else in your code:
BroadService.sharedInstance.fetchBroadcasts { (games) in
if let games = games {
let game = games[indexPath]
let videoLink = game.videoLink
BroadService.sharedInstance.storedVideoLink = videoLink
}
}
// now you can access it from anywhere as
// BroadService.sharedInstance.storedVideoLink
This way it all stays cohesive in the same class. You can even add a getter method for storedVideoLink so you don't have to access it directly, and in this method you could state that if the string is nil then you fetch the data, store the link to the string, and then return the string.
You could create a file with a struct called something like Global and create a static var and set that inside your completion block once you have fetched the games.
Here is an example.
struct Global {
static var games:[Any]? = nil
static func setGames(games:[Any]) {
Global.games = games
}
}
Then you fetch the data once upon load of the app or somewhere before you use the Global and set that property:
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {
completion(nil)
return
}
do {
let games = try JSONDecoder().decode([Games].self, from: data)
Global.setGames(games: games)
completion(games)
} catch let jsonErr {
print("Error deserializing json:", jsonErr)
completion(nil)
}
}.resume()
Please note that this will make the Global.games accessible from everywhere but it will also not be a constant so you should be careful not to override it.
This way Global.games will be accessible from anywhere.
I'm trying to set text on a label from api, but it seems that the function doesn't even get called. Please refer to the snippet below. Is there anything wrong with it?
EDIT: typealias DownloadComplete = () -> ()
var date: String = ""
override func viewDidLoad() {
super.viewDidLoad()
timeLbl.text = date
// Do any additional setup after loading the view.
}
func downloadTimeData(completed: #escaping DownloadComplete) {
//Downloading forecast weather data for TableView
Alamofire.request(APIURL).responseJSON { response in
let result = response.result
if let dict = result.value as? Dictionary<String, AnyObject> {
if let currentDate = dict["fulldate"] as? String {
self.date = currentDate
print(self.date)
print("xxx")
}
}
completed()
}
}
I figured it out with simpler and easier way, through the alamofire documetation.
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(APIURL).responseJSON { response in
print(response.result) // result of response serialization
let result = response.result
if let dict = result.value as? Dictionary<String, AnyObject> {
let currentDate = dict["fulldate"] as? String
self.timeLbl.text = currentDate
}
}
}
In the code you posted you are not calling downloadTimeData(completed:) anywhere.
You can do that in viewDidAppear(_:) for example:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
downloadTimeData {
// The request has completed
timeLbl.text = date
}
}
Note that you may need to change the call slightly, depending on how DownloadComplete is defined.
You are setting timeLbl.text immediately on page load in viewDidLoad, but you haven't told the app to do anything else.
You have to move downloadTimeData to viewDidLoad, and in the completion, set 'timeLbl.text = date'
You have to set some sort of text place holder or loader while your call is being made, because you can't guarantee that it is instant.
Are we setting one label? Or a whole tableview of labels?
I changed some syntax to be "swiftier"
var date = ""
override func viewDidLoad() {
super.viewDidLoad()
//call downloadTimeData here
downloadTimeData() {
//once we are in completion, this means internet call finished, so set label now
self.timeLbl.text = date
}
}
func downloadTimeData(completed: #escaping DownloadComplete) {
//Downloading forecast weather data for TableView
Alamofire.request(APIURL).responseJSON { response in
guard let dict = response.result.value as? [String: AnyObject], let currentDate = dict["full date"] as? String else {
//handle error here if response fails to give you good data
completed()
return
}
self.date = currentDate
print(self.date)
print("xxx")
completed()
}
}
I am trying to pull information from a personal raspberry pi web server and output it within an iOS app. Within the web server I have a php file with the address localhost/test_data. This php file creates a JSON formatted webpage with the data that I want to access. After I get the data at the URL via my GetDataAtURL function, I use the output as an input to the function DataSerialization. The URL I am currently using is a test URL, and I will substitute in my actual URL once it is working. Whenever I call the data DataSerialization function with input "data" (which is the output of the GetDataAtURL function), I receive the error "EXC_BAD_INSTRUCTION(code=EXC_1386_INVOP, subcode=0x0".Why am I receiving this error? My code is as follows:
import UIKit
class ViewController: UIViewController {
var data : NSData!
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "https://api.github.com/users/mralexgray/repos")
GetDataAtURL(url!) { (resultData) -> Void in
print(resultData)
self.data = resultData
}
// print(self.data)
DataSerialization(data)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
func GetDataAtURL (url : NSURL, completion:(resultData: NSData?) -> Void) {
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(url) { (data,_,error) -> Void in
if let error = error {
print(error.localizedDescription)
}
completion(resultData: data)
}
dataTask.resume()
}
func DataSerialization (input: NSData) {
do {
let json = try NSJSONSerialization.JSONObjectWithData(input, options: []) as! [String: AnyObject]
if let name = json["name"] as? [String] {
print(name)
}
if let full_name = json["full_name"] as? [String] {
print(full_name)
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
}
You are setting self.data in an asynchronous callback. That means the data will be stored at some unknown point in the future after you start reading the data. When you try to process the data, it hasn't been set yet.
Best to find some code samples to see how this is done.