Completionhandling in webrequest session - swift

I´ve a webrequest with jsonserialization, after that, a for-in fetch process.
In whole this takes approximately 5-7 seconds.
After that i want to refersh my tableview in Viewcontroller.
The scheme of the function looks like this.
public struct Container {
let name: String
let symbol: String
let rank: String
}
public var dataArray = [Container]()
func fetchNewData() {
var view = ViewController()
// WebbRquest...
// Json serialization...
// the following list is much longer, will take a while...
for items in json {
let name = items["name"] as? AnyObject;
let symbol = items["symbol"] as? AnyObject;
let rank = items["rank"] as? AnyObject;
let result = Container(name: name! as! String, symbol: symbol! as! String,rank: rank! as! String)
dataArray.append(result)
}
// Now, after alle the work is done, i want to reload the tableview in Viewcontrller:
view.reload()
// Here i´m getting error, because nothing will be executed after return.
}
How can I call the reload function, after the webrequest process is finished? Because after the return, the function doesn´t execute anything anymore.
And no other function will "know" when the fetchNewData() function is finished.
Thanks for any help!
#IBAction func updateButton(_ sender: Any) {
fetchNewData()
}
According Phillipps suggestion, I had to modify the #IBAction func a little bit.
But now it´s working. Awesome!
Here the full working version:
public struct Container {
let name: String
let symbol: String
let rank: String
}
public var dataArray = [Container]()
func fetchNewData(completion:#escaping ([Container])->()) {
var view = ViewController()
// WebbRquest...
// Json serialization...
// the following list is much longer, will take a while...
for items in json {
let name = items["name"] as? AnyObject;
let symbol = items["symbol"] as? AnyObject;
let rank = items["rank"] as? AnyObject;
let result = Container(name: name! as! String, symbol: symbol! as! String,rank: rank! as! String)
dataArray.append(result)
}
completion(dataArray)
}
This is the actionFunc:
#IBAction func upDateButton(_ sender: Any) {
let data = dataArray
fetchNewData() {_ in (data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

Here's a start. It will be vague because I'm making guesses about code I can't see, but you may be able to convert it to your own needs.
Change the fetch function so that it takes a closure as a parameter:
func fetchNewData(completion:([Container])->()) {
...note that the closure will accept the data array when it's called.
After you have your json all parsed, you then invoke the closure:
dataArray.append(result)
}
completion(dataArray)
The "magic" is in the view controller where you tell fetchNewData what to do when it's finished. Something like:
#IBAction func updateButton(_ sender: Any) {
fetchNewData() {(data)
// Save the data where the view controller can use it
self.tableArray = data
// Main queue for UI update
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Note that the closure is written in the view controller, so self is the view controller. This means no need to create a second (useless) controller inside the fetch.

Related

Passing data between interface controller in WatchKit

I have issue with implementing data passing between two WatchKit interface controllers. I want to pass data from first interface controller to another one. The first interface controller is working fine without any issue, but the second interface controller not getting the data from the first one.
Here is my struct :
struct gameStruct: Codable {
var id: String
var image: String
var gameName: String
var gameDate: String
var gameVideo: String
var gameSite: String
}
Here is the code in first interface controller:
var gameWatchArray = [gameStruct]()
let getDataURL = "http://ya-techno.com/gameApp/gameData.php"
class InterfaceController: WKInterfaceController {
#IBOutlet var tableView: WKInterfaceTable!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
let URLgame = URL(string: getDataURL)
URLSession.shared.dataTask(with: URLgame!) { (data, response, error) in
do {
guard let data = data else { return }
gameWatchArray = try JSONDecoder().decode([gameStruct].self, from: data)
} catch {
print("error parse")
}
DispatchQueue.main.async {
self.tableView.setNumberOfRows(gameWatchArray.count, withRowType: "gameRow")
for (gameNameIndex, game) in gameWatchArray.enumerated() {
let row = self.tableView.rowController(at: gameNameIndex) as! gameRow
let url = NSURL(string: "http://www.ya-techno.com/gamesImage/\(game.image)")
guard let data = NSData(contentsOf: url! as URL) else { return }
row.gameImage.setImageData(data as Data)
}
for index in gameWatchArray {
index.gameName
index.gameDate
index.image
print("JSON V3 array is :\(index.gameName)")
}
print("JSON V3 array is :\(gameWatchArray.count)")
}
}.resume()
}
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
self.pushController(withName: "showDetails", context: gameWatchArray[rowIndex])
}
and here is the Detail interface in my project:
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let detailData = context as? String {
gameNameLabel.setText(detailData)
}
}
I'm using json to parse data.
The issue is that you are passing a gameStruct instance using self.pushController(withName: "showDetails", context: gameWatchArray[rowIndex]) from your first interface controller to your second one, but then you are trying to cast gameStruct to String, which will obviously fail.
You should modify awake(withContext:) in your second interface controller to conditionally cast context to gameStruct and then access the gameName property of that struct when assigning the String name to the label's text.
In the future you should always handle the cases when a conditional casting fails so that you can find issues more easily by printing a message in case of a failed cast. Or if you are 100% sure that context will always be of a certain type, you can even do force casting, which will enable you to catch programming errors early on in the development stage.
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let gameDetails = context as? gameStruct {
gameNameLabel.setText(gameDetails.gameName)
} else {
print("Passed context is not a gameStruct: \(context)")
}
}
You should also conform to the Swift naming convention, which is UpperCamelCase for type names, so change gameStruct to GameStruct.

Swift, Firebase - Get variable inside observe(.childAdded) closure [duplicate]

I'm trying to use Firebase to set the number of cells in my CollectionView. I tried to create a local variable and set that to the same value as the Firebase variable but when I try use it outside the function it doesn't work. I also tried setting it in ViewWillAppear but it didn't work.
I set the nav bar title to view the value. When it was set in the closure I got the correct value, when I wrote that outside the closure (after the firebase function), it gave a value of 0.
I'm using swift 3
override func viewWillAppear(_ animated: Bool) {
FIRDatabase.database().reference(withPath: "data").child("numCells").observeSingleEvent(of: .value, with: { (snapshot) in
if let snapInt = snapshot.value as? Int {
// self.navigationItem.title = String(snapInt)
self.numCells = snapInt
}
}) { (error) in
print(error.localizedDescription)
}
self.navigationItem.title = String(numCells)
}
...
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return numCells
}
Firebase is asyncronous and the data is only valid when it's returned from Firebase within the closure.
FIRDatabase.database().reference(withPath: "data").child("numCells")
.observeSingleEvent(of: .value, with: { snapshot in
if let snapInt = snapshot.value as? Int {
self.navigationItem.title = String(snapInt)
}
})
Expanding from that, suppose we want to populate an array to be used as a dataSource for a tableView.
class ViewController: UIViewController {
//defined tableView or collection or some type of list
var usersArray = [String]()
var ref: FIRDatabaseReference!
func loadUsers() {
let ref = FIRDatabase.database().reference()
let usersRef = ref.child("users")
usersRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot {
let userDict = child as! [String: AnyObject]
let name = userDict["name"] as! string
self.usersArray.append[name]
}
self.myTableView.reloadData()
})
}
print("This will print BEFORE the tableView is populated")
}
Note that we populate the array, which is a class var, from within the closure and once that array is populated, still within the closure, we refresh the tableView.
Note that the print function will happen before the tableView is populated as that code is running synchronously and code is faster than the internet so the closure will actually occur after the print statement.

UISearchBar textDidChange data from plist

I'd like to search through items of my plist. The plist consists of an array of dictionaries. Each key/value represents Strings/Ints, etc but that isn't important.
As you'll see in the tableViewController class below, I've currently got an array that I have typed. I know I need to make an array of objects/items from my plist but I can't work out how to reference objects from the plist in the view controller.
View controller.swift file:
import UIKit
class TableViewController: UITableViewController, UISearchResultsUpdating {
var array = ["Example 1", "Example 2", "Example 3"]
var filteredArray = [String]()
var searchController = UISearchController()
var resultsController = UITableViewController()
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: resultsController)
tableView.tableHeaderView = searchController.searchBar
searchController.searchResultsUpdater = self
resultsController.tableView.delegate = self
resultsController.tableView.dataSource = self
}
//Added func to update search results
func updateSearchResults(for searchController: UISearchController) {
filteredArray = array.filter({ (array:String) -> Bool in
if array.contains(searchController.searchBar.text!) {
return true
} else {
return false
}
})
resultsController.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension TableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == resultsController.tableView {
return filteredArray.count
} else {
return array.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
if tableView == resultsController.tableView {
cell.textLabel?.text = filteredArray[indexPath.row]
} else {
cell.textLabel?.text = array[indexPath.row]
}
return cell
}
}
I've tried solving this by creating an object class from a tutorial on plists. It uses the example of a periodic table of elements:
import UIKit
struct Element {
enum State: String {
case Solid, Liquid, Gas
}
let atomicNumber: Int
let atomicWeight: Float
let discoveryYear: String
let group: Int
let name: String
let period: Int
let radioactive: Bool
let state: State
let symbol: String
// Position in the table
let horizPos: Int
let vertPos: Int
}
extension Element {
enum ErrorType: Error {
case noPlistFile
case cannotReadFile
}
/// Load all the elements from the plist file
static func loadFromPlist() throws -> [Element] {
// First we need to find the plist
guard let file = Bundle.main.path(forResource: "Element", ofType: "plist") else {
throw ErrorType.noPlistFile
}
// Then we read it as an array of dict
guard let array = NSArray(contentsOfFile: file) as? [[String: AnyObject]] else {
throw ErrorType.cannotReadFile
}
// Initialize the array
var elements: [Element] = []
// For each dictionary
for dict in array {
// We implement the element
let element = Element.from(dict: dict)
// And add it to the array
elements.append(element)
}
// Return all elements
return elements
}
/// Create an element corresponding to the given dict
static func from(dict: [String: AnyObject]) -> Element {
let atomicNumber = dict["atomicNumber"] as! Int
let atomicWeight = Float(dict["atomicWeight"] as! String) ?? 0
let discoveryYear = dict["discoveryYear"] as! String
let group = dict["group"] as! Int
let name = dict["name"] as! String
let period = dict["period"] as! Int
let radioactive = dict["radioactive"] as! String == "True"
let state = State(rawValue: dict["state"] as! String)!
let symbol = dict["symbol"] as! String
let horizPos = dict["horizPos"] as! Int
let vertPos = dict["vertPos"] as! Int
return Element(atomicNumber: atomicNumber,
atomicWeight: atomicWeight,
discoveryYear: discoveryYear,
group: group,
name: name,
period: period,
radioactive: radioactive,
state: state,
symbol: symbol,
horizPos: horizPos,
vertPos: vertPos)
}
}
And in the viewController class, instead of having
var array = ["Example 1", "Example 2", "Example 3"]
I've tried variations of
var array = Element["name"]
and
var array = elements.name
But they obviously don't work because the reference to the plist is in the object class.
If anyone has any idea on how to solve this using swift 3/xcode 8 I would be very appreciative!!
I hope your question is still relevant. As I understand, you can't filter your array, right? If so, I recommend you to take a look at this and this tutorials. Both of them are representing a little bit different approaches to load filtered array, but it doesn't matter much, they work.
P.S. I don't recommend you to make a special tableView for searching, if you want to customize it hereafter, because you will have to do it programmaticly later.
It will be more efficiant to do like this:
If searchController.isActive {
// do some stuff
} else { // another stuff }
But it is just my opinion. I hope my question will help ;).
Thanks for the links Oleg. They were really good!!
It wasn't so much the filtering I had a problem with but actually parsing objects from my plist into a tableview. It took me a few days but I found an answer in case other people were also having the same problem. Keep in mind I'm reasonably new to this and so it might not be the best/perfect way of doing it but it works.
In viewDidLoad, I made a reference to the plist using the following code:
let path = Bundle.main.path(forResource: "Elements", ofType: "plist")
let dictArray = NSArray(contentsOfFile: path!)
I'm not sure of the relevance but I'm pretty sure this next bit is needed if the plist ever needed to be updated. (Also in viewDidLoad)
for elementItem in dictArray! {
let newElement : Element = Element(state:((elementItem as AnyObject).object(forKey: "state")) as! String,
atomicNumber:((elementItem as AnyObject).object(forKey: "atomicNumber")) as! Int,
atomicWeight:((elementItem as AnyObject).object(forKey: "atomicWeight")) as! Float,
discoveryYear:((elementItem as AnyObject).object(forKey: "discoveryYear")) as! String
etc. for each of the keys in the dictionary in the same order as in the Element object in the question. Then (also in viewDidLoad):
elementsArray.append(newElement)
Then its pretty easy, in cellForRow I just have to make a new variable that refers back to the object and I can call up each/any of the associated dictionary keys. E.g.:
let element : Element
cell.textLabel!.text = element.name
cell.detailTextLabel?.text = element.symbol
return cell
Like I said, I know its not perfect but it worked for me. I've heard its not best practice to put too much information in viewDidLoad, so someone else might be able to confirm or provide a better answer.

Getting a let value outside a function

Hello I'm new at swift programming and i want to get label value from loadData() to use use for my path (reference) to my database on dbRef!.child(place+"/placeLabel"). What I mean?! I read data that are on "Dio Con Dio" node. That happens because the value place is let place = "Dio Con Dio". So, my application doesn't load data from "Paradosiako - Panorama" node. I want to load everything from database and i thought that if i could make place value to change to the next node, it could read all the data.
import UIKit
import FirebaseDatabase
class PlacesTableViewController: UITableViewController {
//MARK: Properties
#IBOutlet weak var placesTableView: UITableView!
//database reference
var dbRef:FIRDatabaseReference?
var places = [Places]()
private var loadedLabels = [String: String]()
private var loadedRatings = [String: Int]()
//handler
var handle:FIRDatabaseHandle?
override func viewDidLoad() {
super.viewDidLoad()
dbRef = FIRDatabase.database().reference()
// Loads data to cell.
loadData()
}
private func loadData() {
let place = "Dio Con Dio"
dbRef!.child(place+"/placeLabel").observe(.childAdded, with: {
(snapshot) in
let label = snapshot.value as! String
self.updatePlace(snapshot.key, label: label)
})
dbRef!.child(place+"/rating").observe(.childAdded, with: {
(snapshot) in
let rating = snapshot.value as! Int
self.updatePlace(snapshot.key, rating: rating)
})
}
private func updatePlace(_ key: String, label: String? = nil, rating: Int? = nil) {
if let label = label {
loadedLabels[key] = label
}
if let rating = rating {
loadedRatings[key] = rating
}
guard let label = loadedLabels[key], let rating = loadedRatings[key] else {
return
}
if let place = Places(name: label, rating: rating) {
places.append(place)
placesTableView.reloadData()
}
}
}
(This question is a follow up from this one.)
It's difficult to explain this in a comment, so I'll address your current issue here, but Paulo answered your question, I believe. You could even open a new question on this issue.
Here's the code you're having an issue with:
for (key, rating) in ratings.value as! [String: Int] {
self.updatePlace(key, rating: rating)
}
There's nothing wrong with this code, as long as ratings.value is filled with string: integer pairs. Your error occurs because somehow a there is a key missing. (the runtime found a NSNull, so it couldn't convert the data to a dictionary)
You need to set a breakpoint where you are constructing the array in:
placeSnapshot.childSnapshot(forPath: "rating")
(which is called ratings here/locally). If is ok to ignore entries with no key, then skip them and don't append them to your array. But it's more likely you have an error and need to find out why you're adding a null value...
Does that make more sense? Please accept Paulo's answer if he solved an issue, and open a new question with more information about childSnapshot (the code), and someone will be sure help you. (I know I'll try.)
For your new database schema, try this instead:
private func loadData() {
dbRef!.observe(.childAdded) {
(placeSnapshot) in
print("Adding place \(placeSnapshot.key)...")
// Load each label on the "placeLabel" list.
let labels = placeSnapshot.childSnapshot(forPath: "placeLabel")
for (key, label) in labels.value as! [String: String] {
self.updatePlace(key, label: label)
}
// Load each rating in the "rating" list.
let ratings = placeSnapshot.childSnapshot(forPath: "rating")
for (key, rating) in ratings.value as! [String: Int] {
self.updatePlace(key, rating: rating)
}
}
}
The updatePlace function simply groups the label and rating properties, of a given place, that were separately loaded by loadData.
Places removal. Of course, the above code won't handle place removal. To do so, you will need to listen for the childRemoved event as well.

Call a func from another class in swift

I would like to call a function which is coded on another class.
So far I have made a struct on the file structs.swift for my data:
struct defValues {
let defCityName: String
let loadImages: Bool
init(defCity: String, loadImgs: Bool){
self.defCityName = defCity
self.loadImages = loadImgs
}
}
I have made the file Defaults.swift containing:
import Foundation
class DefaultsSet {
let cityKey: String = "default_city"
let loadKey: String = "load_imgs"
func read() -> defValues {
let defaults = NSUserDefaults.standardUserDefaults()
if let name = defaults.stringForKey(cityKey){
print(name)
let valuesToReturn = defValues(defCity: name, loadImgs: true)
return valuesToReturn
}
else {
let valuesToReturn = defValues(defCity: "No default city set", loadImgs: true)
return valuesToReturn
}
}
func write(city: String, load: Bool){
let def = NSUserDefaults.standardUserDefaults()
def.setObject(city, forKey: cityKey)
def.setBool(load, forKey: loadKey)
}
}
in which I have the two functions read, write to read and write data with NSUsersDefault respectively.
On my main ViewController I can read data with:
let loadeddata: defValues = DefaultsSet().read()
if loadeddata.defCityName == "No default city set" {
defaultCity = "London"
}
else {
defaultCity = loadeddata.defCityName
defaultLoad = loadeddata.loadImages
}
But when I try to write data it gives me error. I use this code:
#IBOutlet var settingsTable: UITableView!
#IBOutlet var defaultCityName: UITextField!
#IBOutlet var loadImgs: UISwitch!
var switchState: Bool = true
#IBAction func switchChanged(sender: UISwitch) {
if sender.on{
switchState = true
print(switchState)
}else {
switchState = false
print(switchState)
}
}
#IBAction func saveSettings(sender: UIButton) {
DefaultsSet.write(defaultCityName.text, switchState)
}
You need an instance of the DefaultsSet class
In the view controller add this line on the class level
var setOfDefaults = DefaultsSet()
Then read
let loadeddata = setOfDefaults.read()
and write
setOfDefaults.write(defaultCityName.text, switchState)
The variable name setOfDefaults is on purpose to see the difference.
Or make the functions class functions and the variables static variables and call the functions on the class (without parentheses)
From the code you posted, it seems you either need to make the write method a class method (just prefix it with class) or you need to call it on an instance of DefaultsSet: DefaultsSet().write(defaultCityName.text, switchState).
Another issue I found is that you also need to unwrapp the value of the textField. Your write method takes as parameters a String and a Bool, but the value of defaultCityName.text is an optional, so String?. This results in a compiler error.
You can try something like this:
#IBAction func saveSettings(sender: UIButton) {
guard let text = defaultCityName.text else {
// the text is empty - nothing to save
return
}
DefaultsSet.write(text, switchState)
}
This code should now compile and let you call your method.
Let me know if it helped you solve the problem