How to replace a simple delegate protocol with RxSwift? - rx-swift

I'm looking for the optimal way to avoid defining
protocol SomeTableviewCellDelegate: class {
func didSelectTopic(topic: RTopic)
}
and use RxSwift instead.
I already have defined
var didSelectTopic: Observable<RTopic> {
return _didSelectTopic.asObservable()
}
private let _didSelectTopic = PublishSubject<RTopic>()
var didDeselectTopic: Observable<RTopic> {
return _didDeselectTopic.asObservable()
}
private let _didDeselectTopic = PublishSubject<RTopic>()
I can't find the proper way to emit a topic (I am new to RxSwift).

You trying to pass information about selected items (Correct me if I wrong).
Currently selected items
We don't care about selection / unselection, we care only about items, that selected in each moment of time.
This is probably most common use case, since your user (user of component) doesn't need to have local state.
protocol TopicSelectionProvider {
var selectedTopicsStream: Observable<Set<RTopic>> { get }
}
class MyTableDelegate: NSObject, UITableViewDelegate {
private var selectedTopics = BehaviourRelay<Set<RTopic>>([])
...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var selected = selectedTopics.value
selected.insert(topics[indexPath.row])
selectedTopics.accept(selected)
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
var selected = selectedTopics.value
selected.remove(topics[indexPath.row])
selectedTopics.accept(selected)
}
}
extension MyTableDelegate: TopicSelectionProvider {
var selectedTopicsStream: Observable<Set<RTopic>> {
return selectedTopics.asObservable()
}
}
Selection / Unselection events
We don't care about selected items, we only interested in events them self.
This use case can be used, when you don't need items, but events (for api calls for example).
enum TopicSelectionEvent {
case select(RTopic)
case deselect(RTopic)
}
protocol TopicSelectionProvider {
var topicSelectionStream: Observable<TopicSelectionEvent> { get }
}
class MyTableDelegate: NSObject, UITableViewDelegate {
private var topicSelection = PublishSubject<TopicSelectionEvent>()
...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
topicSelection.onNext(.select(topics[indexPath.row]))
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
topicSelection.onNext(.deselect(topics[indexPath.row]))
}
}
extension MyTableDelegate: TopicSelectionProvider {
var topicSelectionStream: Observable<TopicSelectionEvent> {
return topicSelection.asObservable()
}
}
Both approach valid and have proc and cons. Choose one that suits you best.

Related

Insert information from REST API in a table view

I after asking another question today and fixed my Swift 4.2 code, I realized that I can't debug the app because of some profiles.
Errors shown when Testing application
The app also crashes on launch so there is not much I can do for now. I believe that the issue is how I am getting the information from the webserver (currently my computer). I am quite new to this, so i might have some mistakes in my code, so bear with me.
import UIKit
class InfoTableViewController: UITableViewController {
//MARK: Properties
class Produs {
var nume: String!
var codBare: String!
init(nume: String, codBare: String) {
self.nume = nume
self.codBare = codBare
}
}
var produse = [Produs]()
override func viewDidLoad() {
super.viewDidLoad()
//Load elements from server, theoretically
loadProducts()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return produse.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "InfoTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InfoTableViewCell else {
fatalError("The dequeued cell is not an instance of InfoTableViewCell.")
}
// Fetches the appropriate meal for the data source layout.
let produs = produse[indexPath.row]
cell.nameMain.text = produs.nume
cell.nameInfo.text = produs.codBare
return cell
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
//MARK: Private Methods
private func loadProducts() { //This function retrieves information in a JSON format from the server
var request = URLRequest(url: URL(string: "192.168.0.145")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request, completionHandler: { data, response, error -> Void in
do {
let decoder = JSONDecoder()
let json = try decoder.decode([[String]].self, from: data!)
print(json)
for produs in json {
print(produs)
var nume_prod: String = produs[0] // Numele produsului
var cod_ext: String = produs[1] // Codul de bare al produsului
var prod_obj = Produs(nume: nume_prod, codBare: cod_ext)
self.produse.append(prod_obj)
}
} catch {
print("JSON Serialization error")
}
}).resume()
}
}
I am getting the information from the server in a JSON file that is an array of arrays and looks like this:
[
[
"product1",
"code1"
],
[
"product2",
"code2"
],
[
"product3",
"code3"
]
]
Thank you for your help!
Don't send the JSON as array of arrays, send it as array of dictionaries. It simplifies the decoding considerably.
[
{"name":"product1", "code":"code1"},
{"name":"product2", "code":"code2"},
{"name":"product3", "code":"code3"}
]
Then declare the model as struct and never declare properties as implicit unwrapped optional which are initialized with non-optional values. If you need optionals declare them as regular optional (?) otherwise non-optional
struct Produs {
let name: String
let code: String
}
Replace loadProducts with
private func loadProducts() { //This function retrieves information in a JSON format from the server
let url = URL(string: "192.168.0.145")!
URLSession.shared.dataTask(with: url, completionHandler: {[unowned self] data, response, error in
if let error = error { print(error); return }
do {
self.produse = try JSONDecoder().decode([Produs].self, from: data!)
print(self.produse)
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print(error)
}
}).resume()
}
And replace cellForRowAt with
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InfoTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! InfoTableViewCell
// Fetches the appropriate meal for the data source layout.
let produs = produse[indexPath.row]
cell.nameMain.text = produs.name
cell.nameInfo.text = produs.code
return cell
}

Realm note based App not properly deleting the Correct Object

When a cell is deleted, the item at the end of the list takes the place of the item that just got deleted. This only happens when there are more than 3 items in the list.
In the gif below I delete numbers 3 and 4 which leaves me with numbers 1,2,5 in the simulator. HOWEVER in the Realm file I have numbers 1,2,4. I have no clue why it does this?
Data Model
import Foundation
import RealmSwift
class Item: Object {
#objc dynamic var name = ""
}
View Controller
import UIKit
import RealmSwift
class ListViewController: UITableViewController {
let realm = try! Realm()
var itemArray : Results<Item>?
var item:Item?
override func viewDidLoad() {
super.viewDidLoad()
self.itemArray = realm.objects(Item.self)
}
#IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Item", message: "", preferredStyle: .alert)
alert.view.tintColor = UIColor.red
let action = UIAlertAction(title: "Add Item", style: .default) { (action) in
let newItem = Item()
newItem.name = textField.text!
try! self.realm.write {
self.realm.add(newItem)
}
self.tableView.reloadData()
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Create new item"
textField = alertTextField
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.itemArray!.count//Size of the Array
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)//Asigns the Protocol Cell
let data = self.itemArray![indexPath.row]
cell.textLabel?.text = data.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let item = itemArray?[indexPath.row] {
try! self.realm.write {
self.realm.delete(item)
}
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
You are retrieving an unsorted result set from Realm, which as per documentation does not necessarily retain insertion order after deletions (basically when you remove 3, then 5 is shifted in its place):
Note that the order of Results is only guaranteed to stay consistent when the query is sorted. For performance reasons, insertion order is not guaranteed to be preserved.
So there are two things you can do:
1.) sort the result set
2.) instead of assuming you're only deleting a single object and otherwise have no movements of any sort, you can rely on Realm's own diffing + change set evaluation with a notification token so that you receive a change set for any possible change that happens to the result set.
// see https://realm.io/docs/swift/latest/#collection-notifications
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}

Need multiple tableview cells with different rows

I have one table view using a different cell based on what tab you select. It is working, although the 1st one seems to be controlling the height. I have one row in the first tab but 2 in the second and both just show 1.
I'm wondering if I need to clear dequeueReusableCell on tab select or something like that. Any Ideas?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if activeTab == "tab1" {
return someService.tab1.count
} else {
return someService.tab2.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if activeTab == "tab1" {
if let cell = tableView.dequeueReusableCell(withIdentifier: "Tab1Cell") as? Tab1Cell {
let tab1 = someService.tab1[indexPath.row]
cell.updateView(tab1: tab1)
return cell
} else {
return Tab1Cell()
}
} else {
if let cell = tableView.dequeueReusableCell(withIdentifier: "Tab2Cell") as? Tab2Cell {
let tab2 = someService.tab2[indexPath.row]
cell.updateView(tab2: tab2)
return cell
} else {
return Tab2Cell()
}
}
}
I'm also fixing the scroll with this:
func fixTableViewSize() {
let cells = self.notesTable.visibleCells
var heightOfTableView = 0
for cell in cells {
print("cell")
heightOfTableView += Int(cell.frame.height)
}
self.tableViewHeightConstraint.constant = CGFloat(heightOfTableView)
self.buttonFromTableTopConstraint.constant = CGFloat(heightOfTableView)
}

How to download name and images on one row in ascending order

I have researched this and nothing seems to be working. I am trying to build a recipe app and the the image of the dish & names of the dish (appetizer) are not downloading in order. How can I do this?
Code:
class Appetizers: UITableViewController {
var valueToPass: String!
var valuePassed: String!
var appetizer = [String]()
var images = [UIImage]()
func refresh() {
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
override func viewDidLoad() {
super.viewDidLoad()
// Parse - class - column
let query = PFQuery(className: "Appetizers")
query.orderByAscending("appetizer")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
for object in objects {
let load = object.objectForKey("appetizer") as! String
self.appetizer.append(load)
let imageFile = object["imageFiles"] as! PFFile
imageFile.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in
if error != nil {
print(error)
} else {
if let data = imageData {
self.images.append(UIImage(data: data)!)
self.tableView.reloadData()
}
}
})
self.tableView.reloadData()
}
}
} else {
print("Error: \(error!) \(error!.userInfo)")
}
}
sleep(1)
refresh()
}
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return appetizer.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = appetizer[indexPath.row]
// add image to table
if images.count > indexPath.row {
cell.imageView?.image = images[indexPath.row]
}
return cell
}
// when user taps on cell ...
func getCellLabel () {
let indexPath = tableView.indexPathForSelectedRow!
let currentCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
valueToPass = currentCell.textLabel!.text
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
getCellLabel()
self.performSegueWithIdentifier("0", sender: self)
}
}
When performing asynchronous queries, you have no assurances regarding the order they complete. So, the concept of two separate arrays, one an array of strings and another an array of images will always be problematic.
You could, for example, replace images with a dictionary indexed by the appetizer name, and thus it wouldn't matter what order they complete.
var appetizer = [String]()
var images = [String: UIImage]()
Thus, it might look like:
override func viewDidLoad() {
super.viewDidLoad()
let query = PFQuery(className: "Appetizers")
query.orderByAscending("appetizer")
query.findObjectsInBackgroundWithBlock { objects, error in
guard error == nil, let objects = objects else {
print(error)
return
}
for (index, object) in objects.enumerate() {
let appetizerName = object.objectForKey("appetizer") as! String
self.appetizer.append(appetizerName)
let imageFile = object["imageFiles"] as! PFFile
imageFile.getDataInBackgroundWithBlock { imageData, error in
guard error == nil, let data = imageData else {
print(error)
return
}
// when the image comes in, asynchronously update only that one row
self.images[appetizerName] = UIImage(data: data)
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade)
}
}
// reload the table only once, after all of the `appetizer` entries are created (but likely before the images come in)
self.tableView.reloadData()
}
}
And
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let name = appetizer[indexPath.row]
cell.textLabel?.text = name
cell.imageView?.image = images[name]
return cell
}
Or you could just replace these two separate properties with one that is an array of custom objects (e.g. an Appetizer object that has both a name property and an image property).
But any way you do that, you want to name sure you're not dealing with two separate array.
By the way, but this process of loading all of the images can be problematic if you have a lot of rows. This code is employing "eager" loading of images (loading them whether they're currently required or not). The problem is that images are relatively large assets (in comparison to the string values) and you can run into memory issues, network bandwidth issues, etc.
One generally likes to employ "lazy" loading (e.g. let cellForRowAtIndexPath request the image only when it's needed. For example, let's say you have 200 rows, of which only 12 are visible at one point. You shouldn't be requesting 200 images, but rather only that for the 12 visible ones. If you take the image retrieval out of viewDidLoad and, instead, have cellForRowAtIndexPath request them one at a time, you'll have much better network performance and less demanding memory characteristics.
If you're going to save the images in some structure like the code currently does, at the very least make sure you purge those images upon receiving notification of a memory warning (and, obviously, gracefully handle the re-requesting them in a JIT manner as needed).
I figured out the problem with my table not loading without sleep() ...
I had 'self.tableView.reloadData()' outside of the block.
Rob was very helpful :)

Two functions with one variable in Swift

I have two functions in a view controller. The first function parses JSON and makes an array; another generates a table with the array data. The problem is that it seems that the first function cannot send its array data to the second function.
Here is the code:-
class secondViewController: UIViewController, UITableViewDataSource {
let chartTitle:[String] = ["Name",......]
func parseJSON(){
let url = NSURL(string: "http://00000.us-west-2.elasticbeanstalk.com/index.php?000000")
let request = NSURLRequest(URL: url!)
do {
let data = try NSURLConnection.sendSynchronousRequest(request, returningResponse: nil)
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers)
var name = json["Name"]
var chartContent:[String] = ["\(name)",.....] //Contents of current chart contents
} catch{
//Handle Exception
}
} catch{
//Handle Exception
}
}
override func viewDidLoad() {
parseJSON()
...
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //currnet table information.
let cell = UITableViewCell()
cell.textLabel?.text = chartTitle[indexPath.row] + "\t\t\t\t\t here comes info" + chartContent[indexPath.row]
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return chartTitle.count
}
}
This code has an error at the tableView function:
Use of unresolved identifier 'chartContent'
I tried to declare the variables outside the first function which is right under the class secondViewController but there was another error on UITableViewDataSource.
Any solution for these?
Charttitle is defined outside any procedure, so it's available everywhere. Chartcontent is defined in a block, so it's usable just in it's block
Its because chartContent is a local variable just available to parseJson func only and its scope is till that func block. You have to create this variable the same way you dis chartTitle to be available throughout the class.

Resources