optional / safe loading in swift - ios

My Program's Flow
I have a Class called News Item. in it, I have a method that goes to a server and fetches JSON Data and creates a NewsItem instance that has each of the JSON data details in a for loop using the SWIFTYJson library as follows:
static func downloadNewsItems(completionBlock block : ([NewsItem]) -> ()) {
var newsItems = [NewsItem]()
let url = NSURL(string: "http://a-url-that-has-stuff")
let networkService = NetworkService(url: url!)
networkService.downloadData { (data) -> Void in
let jsonData = JSON(data:data)
for item in jsonData["stories"].arrayValue {
let newsArticle = NewsItem()
newsArticle.storyCategory = item["category"].string
newsArticle.titleText = item["title"].string
newsArticle.paragraph1 = item["paragraph1"].string
newsArticle.paragraph2 = item["paragraph2"].string
newsArticle.featureImage = item["headerImage"].string
newsArticle.storyDate = item["date"].string
newsArticle.majorReference = item["majorReference"].string
newsArticle.fact = item["fact"].string
newsItems.append(newsArticle)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
block(newsItems)
})
}
}
I have a collectionViewController that calls this method, gets the objects and appends them to an array which i then use to load the collection view data.
My Problem
If you look at the code, at this point, every newsArticle must have all the properties of the news item for this to work. I don't like this behaviour. I'm looking at adding more properties to the NewsItem class that aren't required but if available, should be loaded and properly instantiated. For example, I want to add a second image to the News Item class but not every news item will have a second image. If I add a news item that doesn't have 'titleText' for example, to the backend, the code will break.
I want to add this second image feature, and if a news item has a 'second image', then I want to instantiate a UIImageView and add it to the collectionView with this image.
I know I'm supposed to use optionals somehow but i can't quite crack it. I'm using Swift 3. Optionals, I must admit, have been the bane of my existence since i started swift. Any help would be appreciated. Danke!
EDIT
I'm implementing the newsItem class as follows:
class HomeViewController: UIViewController {
var newsItems = [NewsItem]()
override func viewDidLoad() {
super.viewDidLoad()
NewsItem.downloadNewsItems{ (newsItems) -> () in
self.newsItems = []
self.newsItems = newsItems
self.collectionView.reloadData()
}
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return newsItems.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! NewsCollectionViewCell
let story = newsItems[indexPath.item]
cell.configureCell(story)
return cell
}
}
The configureCell method just downloads anything that needs to be downloaded and then adds the text/images/links to the cell properties. Nothing special there.

Related

Use a function that needs UITableView in another view?

In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}

Grabbing images from API

I want to grab images from an api which gives the url of the image, do i need another api call to download the images from their url's? How do i grab every image and append it in an array and display them in my collection view cell. I have declared this outside the class scope
var videoArray = [funnyVideos]()
Alamofire.request(_url).responseJSON { (response) in
print(response.result.value!)
if let dict = response.result.value as? Dictionary<String, AnyObject> {
if let result = dict["result"] as? Dictionary<String, AnyObject> {
if let data = result["data"] as? [Dictionary<String, AnyObject>] {
for obj in data {
let mainObj = funnyVideos()
mainObj.parseData(from: obj)
videoArray.append(mainObj)
// print (obj)
}
if let image1 = data[0]["image"] as? String {
print(image1)
}
self.collection.reloadData()
}
}
}
completed()
}
this function is another class: funny videos
func parseData(from: Dictionary<String, AnyObject>){
if let name = from["title"] as? String{
self._name = name.capitalized
}
}
this func configure cell is another class which holds the outlets for the label and imageview of collection cell.
func configureCell(_ funny : funnyVideos) {
self.funny = funny
nameLabel.text = self.funny.name.capitalized
// imageView.image = UIImage()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
fun = videoArray[indexPath.row]
cell.configureCell(fun)
}
what options do I have here to grab them from a url in the UIImage container? and print(image1) prints the url of the image.
Try it,
You can use AlamofireImage for downloading image and set to the collectionview.
Yes, you have to make another api call on image's url. You can use AlamofireImage or sdwebimage library for download images.
First hit Api with normal Alamofire library and get link of all images and save them in dict or array . Then make another api call using AlamofireImage or sdwebimage.
you should try sdwebimage becauz it is easy
You should use a Swift library called Kingfisher
It will handle caching as well as downloading of the images when you provide the URLs.
Here is the link for getting started. Cheat Sheet
Yes you need to call the url of images that fetch from json, to get image from url you can either use
use like this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let identifier = cellIdentifier else{
fatalError("Cell identifier not provided")
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier ,
for: indexPath) as UICollectionViewCell
let imageUrl = arrayImage[indexPath.row]
cell.imageView.setImage(url:"imageUrl")
return cell
}
https://github.com/rs/SDWebImage
https://github.com/onevcat/Kingfisher

Better Understanding Reference vs. Value Types in Swift

After reading the Swift documentation and various online tutorials, it still a little hard to wrap my head around reference types versus value types.
I'm following a tutorial from a Swift TDD book to understand OOP architecture in Swift. The tutorial is based on creating a to do list app. In the beginning of the book we created a struct to resemble each to do item. Next we created an class called ItemManger to manage the array that holds to do items and another array to hold checked off to do items. I can understand the idea of to do items being made from a struct because its a value type which creates a new instance every time its instantiated and the itemManager being made from a class since we only need one itemManger to keep track of to do items. The question that I've come up with is when we create an instance of the ItemManager type (which is a class) inside another class or view controller, will this refer to the same class we created before in which we'll be able to access the to do item arrays?
Before this book, I assumed in order to keep track of variables from a class, we have to mark them as static.
Here is the itemManager class:
import Foundation
class ItemManager {
var toDoCount: Int {return toDoItems.count }
var doneCount: Int {return doneItems.count }
private var toDoItems: [ToDoItem] = []
private var doneItems: [ToDoItem] = []
func add(item: ToDoItem) {
if !toDoItems.contains(item){
toDoItems.append(item)
}
}
func checkItem(at index: Int) {
let item = toDoItems.remove(at: index)
doneItems.append(item)
}
func doneItem(at index: Int) -> ToDoItem{
return doneItems[index]
}
func item(at index: Int) -> ToDoItem{
return toDoItems[index]
}
func removeAll(){
toDoItems.removeAll()
doneItems.removeAll()
}
}
Here is another class where we create an instance variable of the ItemManager type:
import UIKit
enum Section: Int {
case toDo
case done
}
class ItemListDataProvider: NSObject, UITableViewDataSource, UITableViewDelegate {
var itemManager: ItemManager?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let itemManager = itemManager else{return 0}
guard let itemSection = Section(rawValue: section)else{ fatalError() }
let numberOfRows: Int
switch itemSection {
case .toDo:
numberOfRows = itemManager.toDoCount
case .done:
numberOfRows = itemManager.doneCount
}
return numberOfRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
guard let itemManager = itemManager else { fatalError() }
guard let section = Section(rawValue: indexPath.section) else { fatalError() }
let item: ToDoItem
switch section {
case .toDo:
item = itemManager.item(at: indexPath.row)
case .done:
item = itemManager.doneItem(at: indexPath.row)
}
cell.configCell(with: item)
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
}
In the code you posted you are not creating an instance of ItemManager.
Here is another class where we create an instance variable of the ItemManager type:
ItemListDataProvider can have a ItemManager but it does not create one. Creating an instance of a class works by calling it's constructor like this:
// Creates an instance of ItemManager and assigns it to itemManager
let itemManager = ItemManager()
Because you did not show where your item manager is created, the question
will this refer to the same class we created before in which we'll be able to access the to do item arrays?
can not really be answered. Where did you create an instance of ItemManager and what did you do with it?
Here is an example:
let itemManagerA = ItemManager()
let itemListDataProviderA() = ItemListDataProvider()
itemListDataProviderA.itemManager = itemManagerA
let itemListDataProviderB() = ItemListDataProvider()
itemListDataProviderB.itemManager = itemManagerA
In this example both ItemListProviders have the same ItemManager and thus have access to the same item arrays.
On the contrary if you are doing something like this:
let itemManagerA = ItemManager()
let itemListDataProviderA() = ItemListDataProvider()
itemListDataProviderA.itemManager = itemManagerA
let itemManagerB = ItemManager() // <-- This creates a SECOND instance of ItemManager
let itemListDataProviderB() = ItemListDataProvider()
itemListDataProviderB.itemManager = itemManagerB // <-- We use the SECOND instance instead of the first one for itemListDataProviderB
both ItemListProviders have different instances of ItemListProvider and do not have access to the same items.

Swift generic closure

I'm building a library for static table views and it works fine, but I encountered a problem with generic closures.
It looks like this so far:
orderForm = Form(tableView: orderTable) { f in
f.section { s in
s.footer("Při platbě nejsou účtovány žádné další poplatky.")
s.cell("Selection")
.configure { (cell, path) in
let c = cell as! ProfileSelectionCell
c.titleLabel?.text = "Způsob platby"
c.detailLabel?.text = self.paymentType.first
}
s.cell("Selection")
.configure { (cell, path) in
let c = cell as! ProfileSelectionCell
c.titleLabel?.text = "Balíček"
c.detailLabel?.text = "20 kr. za 1000 Kc"
}.selected { path in
}
}
}
I wanna have the "cell" variable already cast to appropriate type, in this case ProfileSelectionCell.
Here is the source for the cell class:
class Cell {
internal let id: String
internal var configure: ((cell: UITableViewCell, path: NSIndexPath) -> Void)?
internal var selected: ((path: NSIndexPath) -> Void)?
init(id: String) {
self.id = id
}
func configure(config: ((cell: UITableViewCell, path: NSIndexPath) -> Void)?) -> Self {
self.configure = config
return self
}
func selected(selected: (path: NSIndexPath) -> Void) -> Self {
self.selected = selected
return self
}}
My problem is that if I make the configure method generic, it is not possible to store the config closure to the cell variable and if I make the whole cell generic, I can't save the cell to an array in Section class and so on..
Is this solvable in any way?
You can make the Cell class generic, e.g.
class Cell<T : UITableViewCell> {
}
and then use T instead of every UITableViewCell.
Unfortunately you would have to have the same in both Section and Form classes, too. That would work for tables with one type of cells but it won't probably work for tables with multiple cell types. In that case you will always need casting somewhere.

Populate a collection view with firebase data

I'm trying to recreate a new firebase project where you populate a table view with data from firebase realtime database that contain links to images in firebase storage.
I can populate the tutorial project which is a table view with firebase data. But with my current project it is a collection view inside an extension.
I've narrowed down the issue to my variables
var ref: FIRDatabaseReference!
var messages: [FIRDataSnapshot]! = []
var msglength: NSNumber = 10
private var _refHandle: FIRDatabaseHandle!
specifically
var messages: [FIRDataSnapshot]! = []
Which I think is an array of my data I get from firebase
I then call a function that should populate that array in my viewdidload()
func loadPosts(){
self.messages.removeAll()
// Listen for new messages in the Firebase database
_refHandle = self.ref.child("messages").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
//print("1")
self.messages.append(snapshot)
//print(self.messages.count)
})
}
The issue happens when I try to populate my collections view since I want horizontal scrolling I use an extension. In the extension I find that my array of values is always 0, but in my loadPosts() function the count of my >array is the same value as the amount of posts I have in firebase.
extension HomeViewController : UICollectionViewDataSource
{
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
print(messages.count)
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(StoryBoard.CellIdentifier, forIndexPath: indexPath) as! InterestCollectionViewCell
// Unpack message from Firebase DataSnapshot
let messageSnapshot: FIRDataSnapshot! = self.messages[indexPath.row]
let message = messageSnapshot.value as! Dictionary<String, String>
let name = message[Constants.MessageFields.name] as String!
if let imageUrl = message[Constants.MessageFields.imageUrl] {
if imageUrl.hasPrefix("gs://") {
FIRStorage.storage().referenceForURL(imageUrl).dataWithMaxSize(INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
cell.featuredImageView?.image = UIImage.init(data: data!)
}
} else if let url = NSURL(string:imageUrl), data = NSData(contentsOfURL: url) {
cell.featuredImageView?.image = UIImage.init(data: data)
}
cell.interestTitleLabel?.text = "sent by: \(name)"
}
return cell
}
}
Should I not be using FIRDataSnapshot? If so which is the correct one to use? Or should I approach the project in another form not using extensions?
You are correctly inserting the items into your array within the completion block, but you are missing a call to reload your collectionView.

Resources