Swift: Lazy load variable vs MVC model? - ios

I'm building an app with MVC Model.
I use lazy load technical to fill up a variable. (Model)
And this variable is being by one UIViewController (Controller)
But i don't know how to reload or trigger the view controller when the model action is finished. Here is my code
Model (lazy load data)
class func allQuotes() -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
}
return quotes
}
And the part of view controller
class Index:
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var quotes = IndexQuotes.allQuotes()
var collectionView:UICollectionView!
override func viewDidLoad() {
This is really serious question, i'm confusing what technic will be used to full fill my purpose?

Since Alamofire works asynchronously you need a completion block to return the data after being received
class func allQuotes(completion: ([IndexQuotes]) -> Void)
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for photoDict in (response.result.value as! [NSDictionary]) {
let photo = IndexQuotes(dictionary: photoDict)
quotes.append(photo)
}
}
completion(quotes)
}
}
Or a bit "Swiftier"
... {
let allPhotos = response.result.value as! [NSDictionary]
quotes = allPhotos.map {IndexQuotes(dictionary: $0)}
}
I'd recommend also to use native Swift collection types rather than NSArray and NSDictionary
In viewDidLoad in your view controller call allQuotes and reload the table view in the completion block on the main thread.
The indexQuotes property starting with a lowercase letter is assumed to be the data source array of the table view
var indexQuotes = [IndexQuotes]()
override func viewDidLoad() {
super.viewDidLoad()
IndexQuotes.allQuotes { (quotes) in
self.indexQuotes = quotes
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}

First of all call the function from inside the viewdidLoad. Secondly use blocks or delegation to pass the control back to ViewController. I would prefer the blocks approch. You can have completion and failure blocks. In completions block you can reload the views and on failure you can use alertcontroller or do nothing.
You can see AFNetworking as an example for blocks.

It's async action, just use a callback here:
class func allQuotes(callback: () -> Void) -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
callback()
}
return quotes
}
In your UIViewController:
var quotes = IndexQuotes.allQuotes() {
self.update()
}
var collectionView:UICollectionView!
override func viewDidLoad() {
update()
}
private func update() {
// Update collection view or whatever.
}
Actually, I strongly don't recommend to use class functions in this case (and many other cases too), it's not scalable and difficult to maintain after some time.

Related

How to re-initialize a lazy property from another view?

I have data that I want to read from disk into memory that takes a nontrivial amount of time.
I want to be able to do two things:
I don't the data to be read every time the view loads.
I want to be able to invoke it from another view.
lazy var data: [String: String] = {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}()
Above code gets initialized only once when the view loads for the first time, which is perfect for eliminating unnecessary computation. The problem is I also want to be able to trigger it from another view when needed.
I tried to trigger re-initialization:
func getData() {
guard let data = readFromDisk() else { return [:] }
data = processData(data: data)
}
and invoke it from another view:
let vc = ViewController()
vc.getData()
but, doesn't work.
I tried to see if I could use static since it's also lazy, but I get an error saying:
Instance member cannot be used on type 'ViewController'
Finally, I tried creating a separate class:
class DataImporter {
var data: [String: String] {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}
func readFromDisk() -> [String: String] {}
func processData(data: [String: String]) -> [String: String] {}
}
and have the lazy property in ViewController:
lazy var importer = DataImporter()
thinking that instantiating a class achieves the dual effect of taking advantage of a lazy property and invoking it when needed:
let vc = ViewController()
vc.importer = DataImporter()
This instantiates the class about a hundred times for some reason which is not ideal.
I would suggest creating a function that loads the data into data and then whenever you need to reload data, simply reassign it.
class DataStore {
lazy var data: [String: String] = loadData()
func readFromDisk() -> Data? {...}
func processData(data: Data) -> [String:String] { ... }
func loadData() -> [String:String] {
guard let data = readFromDisk() else { return [:] }
return processData(data: data)
}
}
let store = DataStore()
let data = store.data // only loaded here
store.data = store.loadData() // reloads the data
If you don't want the loadData function to be exposed, you can also create a separate reloadData function.
class DataStore {
...
func reloadData() {
data = loadData()
}
}
and then instead of doing store.data = store.loadData(), simply call store.reloadData()

iOS Swift 5 Using Data from Cloud Database in View Controllers in TabBarController

The problem: I cannot get data downloaded into arrays in a singleton class to populate table views in two view controllers.
I am writing a bank book iOS app with a Parse backend. I have a login viewController and four other view controllers in a Tab Bar Controller. I have a singleton class that gets data from the Parse server and loads four arrays. I want that data to populate table views in two other view controllers. I want to make as few data calls as possible. The initial view controller is where user enters debits and credits. So my plan was to call GetData class from the viewDidLoad to populate tables in case user visits them without entering a debit or a credit.
When a debit or credit is entered, there is one function where after the debit or credit is saved to Parse server, the GetData class is called again to update the arrays in the GetData class.
The two view controllers access the arrays in the GetData class to fill the tables, and there is a tableView.reloadData() call in the viewDidAppear in each view controller when the view is accessed via the tab controller.
It works intermittently at best. sometimes I get five successful updates and then it keeps displaying old data, then it will suddenly display all the data.
Looking at my cloud DB, all the entries are there when made, and I have verified the viewWillAppear is firing in each view controller who accessed.
What I need is a reliable method to get the data to update in the other view controllers every. time. I will gladly scrap this app and rewrite if needed.
Here is the code of my singleton class:
class GetData {
static let sharedInstance = GetData()
var transactionArray = [String]()
var dateArray = [String]()
var toFromArray = [String]()
var isDebitArray = [String]()
func getdata() {
let query = PFQuery(className:"Transaction")
query.findObjectsInBackground { (objects, error) in
self.transactionArray.removeAll()
self.dateArray.removeAll()
self.toFromArray.removeAll()
self.isDebitArray.removeAll()
print("query fired")
if objects != nil {
for object in objects! {
if let amount = object.object(forKey: "amount") as? String {
if let date = object.object(forKey: "date") as? String {
if let toFrom = object.object(forKey: "toFrom") as? String {
if let isDebit = object.object(forKey: "isDebit") as? String {
self.transactionArray.append(amount)
self.dateArray.append(date)
self.toFromArray.append(toFrom)
self.isDebitArray.append(isDebit)
}
}
}
}
}
}
self.transactionArray.reverse()
self.dateArray.reverse()
self.toFromArray.reverse()
self.isDebitArray.reverse()
dump(self.toFromArray)
}
}
}
Here is a sample of one of the view controllers:
class RecordVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var recordTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
recordTableView.delegate = self
recordTableView.dataSource = self
recordTableView.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
recordTableView.reloadData()
print("recordVC viewWillAppear fired")
}
#IBAction func resetFoundButton(_ sender: Any) {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = recordTableView.dequeueReusableCell(withIdentifier: "RecordCell", for: indexPath) as! RecordCell
cell.amountLabel?.text = "$\(GetData.sharedInstance.transactionArray[indexPath.row])"
cell.dateLabel?.text = "\(GetData.sharedInstance.dateArray[indexPath.row])"
cell.toFromLabel?.text = "\(GetData.sharedInstance.toFromArray[indexPath.row])"
let cellColor = backGroundColor(isDebit: GetData.sharedInstance.isDebitArray[indexPath.row])
cell.backgroundColor = cellColor
cell.backgroundColor = cellColor
return cell
}
func backGroundColor(isDebit:String) -> UIColor{
if isDebit == "false" {
return UIColor.green
} else {
return UIColor.blue
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return GetData.sharedInstance.transactionArray.count
}
}
Thank you
I would say that instead of reloading the tables by calling tableView.reloadData() in viewWillAppear() , after your query execution and data updates in GetData Class , then you should fire a notification or use a delegate to reloadData() in tableview.
Whats happening is that sometimes when the tableView.reloadData() gets called the Data in the singleton class (GetData class) has not yet updated.
func getdata() {
let query = PFQuery(className:"Transaction")
query.findObjectsInBackground { (objects, error) in
self.transactionArray.removeAll()
self.dateArray.removeAll()
self.toFromArray.removeAll()
self.isDebitArray.removeAll()
print("query fired")
if objects != nil {
for object in objects! {
if let amount = object.object(forKey: "amount") as? String {
if let date = object.object(forKey: "date") as? String {
if let toFrom = object.object(forKey: "toFrom") as? String {
if let isDebit = object.object(forKey: "isDebit") as? String {
self.transactionArray.append(amount)
self.dateArray.append(date)
self.toFromArray.append(toFrom)
self.isDebitArray.append(isDebit)
// Here you should fire up a notification to let the 2 ViewControllers know that data has to be reloaded.
}
}
}
}
}
}
self.transactionArray.reverse()
self.dateArray.reverse()
self.toFromArray.reverse()
self.isDebitArray.reverse()
dump(self.toFromArray)
}
}

Wait for JSON parsing in a different class file in swift 3

I created a class as shown in the code below, and as u can see I am parsing a JSON file in the class outside the viewController.
When I create the AllCards object in the view controller obviously return 0 at the beginning but after a while it returns the correct number of cards.
here my questions:
1) How can I wait the object creation before the viewDidLoad so at the view did load the AllCard object will return the correct number of cards?
2) If I add a button in the viewController updating the number of cards it freezes until all the cards have been created. I think because in my code everything is in the main queue. How can I resolve that?
3) Is it a good practice parsing JSON in a separate class like I did?
AllCards class:
import Foundation
import Alamofire
import SwiftyJSON
class AllCards {
var allCard = [Card]()
let dispatchGroup = DispatchGroup()
//gzt the JSON with Alamofire request
let allCardsHTTP: String = "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape"
init() {
dispatchGroup.enter()
Alamofire.request(allCardsHTTP, method: .get).responseJSON { (response) in
if response.result.isSuccess {
let jsonCards : JSON = JSON(response.value!)
print("success")
//create the cards
if jsonCards["messagge"].stringValue != "" {
print(jsonCards["message"].stringValue)
}
else {
for (set, value) in jsonCards {
if jsonCards[set].count != 0 {
for i in 0...jsonCards[set].count - 1 {
let card = Card(id: jsonCards[set][i]["cardId"].stringValue, name: jsonCards[set][i]["name"].stringValue, cardSet: set, type: jsonCards[set][i]["type"].stringValue, faction: jsonCards[set][i]["faction"].stringValue, rarity: jsonCards[set][i]["rarity"].stringValue, cost: jsonCards[set][i]["cost"].intValue, attack: jsonCards[set][i]["attack"].intValue, durability: jsonCards[set][i]["durability"].intValue, text: jsonCards[set][i]["text"].stringValue, flavor: jsonCards[set][i]["flavor"].stringValue, artist: jsonCards[set][i]["artist"].stringValue, health: jsonCards[set][i]["health"].intValue, collectible: jsonCards[set][i]["collectible"].boolValue, playerClass: jsonCards[set][i]["playerClass"].stringValue, howToGet: jsonCards[set][i]["howToGet"].stringValue, howToGetGold: jsonCards[set][i]["howToGetGold"].stringValue, mechanics: [""], img: jsonCards[set][i]["img"].stringValue, imgGold: jsonCards[set][i]["imgGold"].stringValue, race: jsonCards[set][i]["race"].stringValue, elite: jsonCards[set][i]["elite"].boolValue, locale: jsonCards[set][i]["locale"].stringValue)
if jsonCards[set][i]["mechanics"].count > 0 {
for n in 0...jsonCards[set][i]["mechanics"].count - 1 {
card.mechanics.append(jsonCards[set][i]["mechanics"][n]["name"].stringValue)
}
}
else {
card.mechanics.append("")
}
self.allCard.append(card)
}
}
else {
print("The set \(set) has no cards")
}
}
print(self.allCard.count)
}
}
else {
print("No network")
}
self.dispatchGroup.leave()
}
}
}
View Controller:
import UIKit
class ViewController: UIViewController {
let allcards = AllCards()
let mygroup = DispatchGroup()
#IBAction func updateBtn(_ sender: Any) {
print(allcards.allCard.count) //Button is frozen until all the cards have been created then it shows the correct number of cards
}
override func viewDidLoad() {
super.viewDidLoad()
print(allcards.allCard.count) / This returns 0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is an example of completion handler.
First you have to write a function in a single class ex: APICall
func getDataFromJson(allCardsHTTP: String, completion: #escaping (_ success: Any) -> Void) {
Alamofire.request(allCardsHTTP, method: .get).responseJSON { response in
if response.result.isSuccess {
completion(response)
}
}
}
and call this method from any class.
let callApi = APICall()
callApi.getDataFromJson(allCardsHTTP: "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape",completion: { response in
print(response)
})
1) if you pass an object via a UIStoryboard segue, it's set before viewDidLoad() is called. However if you want to wait for UI elements to be ready, I generally go for a didSet on the UI element, you could add a guardstatement checking your object in there if you want.
2) first of all you'll probably want a closure so maybe read 3) first. you're using dispatchGroup.enter() here, DispatchQueue.global.async { } is the usual way to accomplish what you're doing. Add in a DispatchQueue.main.async { } when done if you want, or dip into the main thread in the view controller, up to you really. Look into the differences between [unowned self]
and [weak self] when you have the time.
3) Give your Card object an init(from: JSON) initialiser where it parses its properties from the JSON object you're passing it.
Let the function responsible for the download (Alamofire in your case) live in a different class (like for example APIClient) and give it a download function with a closure in the argument list like completion: ((JSON?) -> ())? to return the JSON object. Let that class download a JSON object and then initialise your Cardobject with the init(from: JSON) initializer you wrote earlier. Note that this isn't an approach fit for use with Core Data NSManagedObjects so if you'll need local storage maybe keep that in mind.
In the end you should be able to build an array of Cards something like this:
APIClient.shared.fetchCards(completion: { cardJSONs in
let cards = [Card]()
for cardJSON: JSON in cardJSONs {
let card = Card(from; JSON)
cards.append(card)
}
}

Why global variable not updating value in swift 3?

This is the class where the value is
class CurrentWeather{
var _date:String!
var _cityName:String!
var _temp:Double!
var _weatherType:String!
var cityName:String{
if _cityName==nil{
_cityName = ""
}
return _cityName
}
var currentTemprature:Double{
if _temp==nil{
_temp = 0.0
}
return self._temp
}
var weathertype:String{
if _weatherType==nil{
_weatherType = ""
}
return _weatherType
}
var date:String{
if _date==nil{
_date = ""
}
let dateFormater=DateFormatter()
dateFormater.dateStyle = .long
dateFormater.timeStyle = .none
let currentDate = dateFormater.string(from: Date())
self._date="\(currentDate)"
return _date
}
func weatherDataDownload(completed : downloadComplete){
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON{response in
if let dict=response.result.value as? Dictionary<String,AnyObject>{
if let name=dict["name"] as? String{
self._cityName = name.capitalized
print(name.capitalized)
}
if let weather=dict["weather"] as? [Dictionary<String,AnyObject>]{
if let main=weather[0]["main"] as? String{
self._weatherType=main.capitalized
print(main.capitalized)
}
}
if let tempr=dict["main"] as? Dictionary<String,AnyObject>{
if let temp=tempr["temp"] as? Double{
let convertedTemp=Double(round(temp-273.15))
self._temp=convertedTemp
print(convertedTemp)
}
}
}
}
completed()
}}
This is the ViewController class
var currentWeatherOj = CurrentWeather()
override func viewDidLoad() {
super.viewDidLoad()
table.delegate=self
table.dataSource=self
currentWeatherOj.weatherDataDownload {
self.updateUIweather()
}
}
func updateUIweather () {
weatherType.text=currentWeatherOj.weathertype
presentDate.text=currentWeatherOj.date
presentLocation.text=currentWeatherOj.cityName
presentTemp.text="\(currentWeatherOj.currentTemprature)"
}
when I try to call in ViewController its showing the default value which I set inside of computed variable other than _date but I am able print values inside the func of weatherDataDownload.I am confused how variables in swift 3 works because of this.
See the comments in the following code sample. You need to move the call to "completed()"
func weatherDataDownload(#escaping completed : downloadComplete) {
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON { response in
// ... leave your code here alone
// put the call to completed() here
completed()
}
// not here
}
When you make the all to Alamofire, it executes it's request on a background thread. When that request completes, it calls the closure that you've defined (the one that starts "response in..."). You don't want to call updateUIweather until that has been done so you put the call to "completed()" inside of the same completion handler.
When the call to completed was outside of that completion handler, it would be called right away... immediately after the Alamofire request was sent (but before it had finished on that background thread). None of the code in the completion handler has run yet so your variables aren't updated yet.
Finally because your completed closure was passed to a block that was then sent off to a background thread, that closure "escapes" the current function. You add the #escaping so that folks reading your code will know that the closure will live on beyond the life of that function.

Populate array of custom classes from two different network requests

In my Swift iOS project, I am trying to populate an array of custom class objects using JSON data retrieved with Alamofire and parsed with SwiftyJSON. My problem, though, is combining the results of two different network request and then populating a UITableView with the resulting array.
My custom class is implemented:
class teamItem: Printable {
var name: String?
var number: String?
init(sqljson: JSON, nallenjson: JSON, numinjson: Int) {
if let n = sqljson[numinjson, "team_num"].string! as String! {
self.number = n
}
if let name = nallenjson["result",0,"team_name"].string! as String! {
self.name = name
}
}
var description: String {
return "Number: \(number) Name: \(name)"
}
}
Here is my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
refresh() {
() -> Void in
self.tableView(self.tableView, numberOfRowsInSection: self.teamsArr.count)
self.tableView.reloadData()
for item in self.teamsArr {
println(item)
}
return
}
self.tableView.reloadData()
}
which goes to the refresh() method:
func refresh(completionHandler: (() -> Void)) {
populateArray(completionHandler)
}
and finally, populateArray():
func populateArray(completionHandler: (() -> Void)) {
SqlHelper.getData("http://cnidarian1.net16.net/select_team.php", params: ["team_num":"ALL"]) {
(result: NSData) in
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(result, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonObject)
self.json1 = json
println(json.count)
for var i = 0; i < json.count; ++i {
var teamnum = json[i,"team_num"].string!
NSLog(teamnum)
Alamofire.request(.GET, "http://api.vex.us.nallen.me/get_teams", parameters: ["team": teamnum])
.responseJSON { (req, res, json, err) in
let json = JSON(json!)
self.json2 = json
self.teamsArr.append(teamItem(sqljson: self.json1, nallenjson: self.json2, numinjson: i))
}
}
completionHandler()
}
}
the first problem I had was that i in the for loop reached 3 and caused errors when I thought it really shouldn't because that JSON array only contains 3 entries. My other main problem was that the table view would be empty until I manually triggered reloadData() with a reload button in my UI, and even then there were problems with the data in the tables.
really appreciate any assistance, as I am very new to iOS and Swift and dealing with Alamofire's asynchronous calls really confused me. The code I have been writing has grown so large and generated so many little errors, I thought there would probably be a better way of achieving my goal. Sorry for the long-winded question, and thanks in advance for any responses!
The Alamofire request returns immediately and in parallel executes the closure, which will take some time to complete. Your completion handler is called right after the Alamofire returns, but the data aren't yet available. You need to call it from within the Alamofire closure - this ensures that it is called after the data became available.

Resources