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

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()

Related

Where to store Decoded JSON array from server and how to access it globally in viewControllers?

Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.

Extracting data from API (JSON format) doesn't save data outside of function call

I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)

How to send MSMessage in Messages Extension?

I want to implement an imessage app, however being new to the messages framework and iMessage apps being such a new thing there aren't many resources. So I am following the WWDC video and using Apples providing sample app for a guide.
I have three views, the MessageViewController which handles pretty much all the functionality and then a CreateViewController and a DetailsViewController.
I am simply trying to create an MSMessage from the CreateViewController and display in the DetailsViewController.. then add to the data.
However I get a crash when trying to create the data.
#IBAction func createAction(_ sender: AnyObject) {
//present full screen for create list
self.delegate?.createViewControllerDidSelectAdd(self as! CreateViewControllerDelegate)
}
The data type I am trying to pass is the dictionary from a struct:
struct data {
var title: String!
var date: Date!
var dictionary = ["title" : String(), "Array1" : [String](), "Array2" : [String]() ] as [String : Any]
}
So here's how things are set up;
MessagesViewController
class MessagesViewController: MSMessagesAppViewController, {
// MARK: Responsible for create list button
func composeMessage(for data: dataItem) {
let messageCaption = NSLocalizedString("Let's make", comment: "")
let dictionary = data.dictionary
func queryItems(dictionary: [String:String]) -> [URLQueryItem] {
return dictionary.map {
URLQueryItem(name: $0, value: $1)
}
}
var components = URLComponents()
components.queryItems = queryItems(dictionary: dictionary as! [String : String])
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "messages-layout-1.png")!
layout.caption = messageCaption
let message = MSMessage()
message.url = components.url!
message.layout = layout
message.accessibilityLabel = messageCaption
guard let conversation = activeConversation else { fatalError("Expected Convo") }
conversation.insert(message) { error in
if let error = error {
print(error)
}
}
}
}
extension MessagesViewController: CreateViewControllerDelegate {
func createViewControllerDidSelectAdd(_ controller: CreateViewControllerDelegate) {
//CreatesNewDataItem
composeMessage(for: dataItem())
}
}
CreateViewController
/**
A delegate protocol for the `CreateViewController` class.
*/
protocol CreateViewControllerDelegate : class {
func createViewControllerDidSelectAdd(_ controller: CreateViewControllerDelegate)
}
class CreateViewController: UIViewController {
static let storyboardIdentifier = "CreateViewController"
weak var delegate: CreateViewControllerDelegate?
#IBAction func create(_ sender: AnyObject) {
//present full screen for create list
self.delegate?.createViewControllerDidSelectAdd(self as! CreateListViewControllerDelegate)
}
}
Would someone show where I am going wrong and how I can send a MSMessage? If I am able to send the message I should then be able to receive and resend.
One issue I see, without being able to debug this myself:
you are setting your components.queryItems to your dictionary var cast as [String:String], but the dictionary returned from data.dictionary is not a [String:String], but a [String:Any]
In particular, dictionary["Array1"] is an array of Strings, not a single string. Same for dictionary["Array2"]. URLQueryItem expects to be given two strings in its init(), but you're trying to put a string and an array of strings in (though I'm not sure that you're actually getting to that line in your queryItems(dictionary:) method.
Of course, your dataItem.dictionary is returning a dictionary with 4 empty values. I'm not sure that's what you want.

Init method for a singleton

I have my file myAPI.swift, and two objet Round and GameStats. My Round object also have an attribute GameStats. So what I want to do is to get the GameStats property that I store inside my users defaults then assign it inside my Round object.
class myAPI: NSObject {
static let sharedInstance = myAPI()
var currentStats: GameStats?
var currentRound: Round?
private init(){
super.init()
self.loadData()
NSLog("Stats have been reload: \(self.currentRound?.gameStats)") // Return nil
// If I try to add this line the app stop running and nothing happens
NSLog("Test Singleton: \(myApp.sharedInstance.currentRound?.gameStats)")
}
func loadData(){
let backupNSData = NSUserDefaults.standardUserDefaults().objectForKey("backupNSData")
if let backupNSData = backupNSData as? NSData{
let backupData = NSKeyedUnarchiver.unarchiveObjectWithData(backupNSData)
if let backupData = backupData as? [String:AnyObject] {
guard let round = backupData["currentRound"] as? Round else {
print("error round loaddata")
return
}
self.currentRound = round
guard let stats = backupData["stats"] as? GameStats else {
print("error guard stats")
return
}
self.currentRound.gameStats = stats
NSLog("Stats reloaded: \(stats)") // This is not nil it works here
}
}
}
When my app crash I call this function to save the data
func backupData(){
var backupData:[String:AnyObject] = [String:AnyObject]()
if let round = self.currentRound {
backupData["currentRound"] = round
ColorLog.purple("Stats saved inside Round \(round.gameStats)")
}
if let stats = self.currentStat {
backupData["stats"] = stats
ColorLog.purple("Stats saved : \(stats)")
}
let backupNSData = NSKeyedArchiver.archivedDataWithRootObject(backupData)
NSUserDefaults.standardUserDefaults().setObject(backupNSData, forKey: "backupNSData")
NSUserDefaults.standardUserDefaults().synchronize()
}
So I have two question,
Is it normal that I can't call my singleton like myApp.sharedInstance.currentRound.id = 5 (for instance) inside the init() (I guess it is but I didn't find anything about that)
Why in my init() method my first NSLog self.currentRound?.gameStats is nil when in the function loadData() it wasn't ? It seems like it's losing its reference since we are leaving the function.
What am I doing right now is adding a currentStats property in my singleton, then when I retrieve data instead of doing self.currentRound.gameStats = stats I do self.currentStats = stats, then self.currentRoud.gameStats = self.currentStats and If I do that it works, I don't really know If I am doing the things right here.
Also my two objects Round and GameStats conform to NSCoding protocol as I implemented #objc func encodeWithCoder and #objc required init?(coder aDecoder: NSCoder) methods for both of them.
Thank for you help.

Swift: Lazy load variable vs MVC model?

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.

Resources