I have a UITableView populated with Product models. There are 2 challenges I'm having right now. The first is I have a quantityLabel on the cells which I want to update when the user presses an increment or decrement button on the cell. Currently to do this, I am updating the model and accepting an entirely new array, which triggers the tableView to reload, which isn't ideal. The second challenge is, when the quantity of a Product goes to 0, that cell should be removed from the tableView with an animation. Currently, I just find the index of the product within the dataSource, remove it and reload the table. I'd like to know the proper Rx way to do this. Here's a sample of my code. Thanks!
struct Product: Equatable {
var i: String
var quantity: Int
init(i: Int, quantity: Int) {
self.i = "item: \(i)"
self.quantity = quantity
}
}
extension Product {
static func ==(lhs: Product, rhs: Product) -> Bool {
return lhs.i == rhs.i
}
}
class ProductSource {
var items: BehaviorRelay<[Product]> = BehaviorRelay<[Product]>(value: [])
var itemsObservable: Observable<[Product]>
init() {
itemsObservable = items.asObservable().share(replay: 1)
items.accept((0..<20).map { Product(i: $0, quantity: 2) })
}
func add(product: Product) {}
func remove(product: Product) {
guard let index = self.items.value.index(of: product) else {return}
// decrement quantity, if quantity is 0, remove from the datasource and update the tableView
}
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var disposeBag = DisposeBag()
var data = ProductSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.rx.setDelegate(self).disposed(by: disposeBag)
data.itemsObservable
.bind(to: tableView.rx.items(cellIdentifier: "productCell", cellType: ProductTableViewCell.self)) { row, element, cell in
cell.nameLabel.text = element.i
cell.quantityLabel.text = String(element.quantity)
}.disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
Both of your challenges can be solved by using the RxDataSources Cocoapod.
Alternatively, you could roll your own like I did here: https://github.com/dtartaglia/RxMultiCounter/blob/master/RxMultiCounter/RxExtensions/RxSimpleAnimatableDataSource.swift
The idea here is to use something (I'm using DifferenceKit) to figure out which elements changed and only reload those particular elements. To do this, you need a specialized data source object, not the generic one that comes with RxCocoa.
Related
I am trying to add an option to add additional student fields inside table so that user can add more than one student name.
But I am confused how to do it using table view.
I am not interested in hiding view with specific number of fields.
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
struct listItems{
var title : String
var isExpandable:Bool
var maxFields :Int
init(title:String,isExp:Bool,mxF:Int) {
self.title = title
self.isExpandable = isExp
self.maxFields = mxF
}
}
#IBOutlet weak var tblListTable: UITableView!
let data : [listItems] = [listItems(title: "Name", isExp: false, mxF: 1), listItems(title: "Student Name", isExp: true, mxF: 20), listItems(title: "Email", isExp: false, mxF: 1)]
override func viewDidLoad() {
super.viewDidLoad()
tblListTable.delegate = self
tblListTable.dataSource = self
self.tblListTable.reloadData()
print("isLoaded")
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRow")
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! ListCell
cell.lblName.text = data[indexPath.row].title
if data[indexPath.row].isExpandable == true {
cell.btnAddField.isHidden = false
print("ishidden")
}
else {
cell.btnAddField.isHidden = true
}
return cell
}
}
List Cell Class
import UIKit
protocol AddFieldDelegate : class {
func addField( _ tag : Int)
}
class ListCell: UITableViewCell {
#IBOutlet weak var btnAddField: UIButton!
#IBOutlet weak var lblName: UILabel!
#IBOutlet weak var txtField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func addField( _ tag : Int){
}
}
You are on the right track creating the AddFieldDelegate. However, rather than implementing the method inside the ListCell class you need to implement it in the ViewController.
First, change the view controller class definition line to:
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource, AddFieldDelegate {
This will allow you to call the delegate method from the view controller. Next, when you are creating your table view cells add the line:
cell.delegate = self
After that, move the method definition of the method addField to the view controller.
So inside of your view controller add:
func addField(titleOfTextFieldToAdd: String, numberAssociatedWithTextFieldToAdd: Int) {
data.append(listItems(title: titleOfTextFieldToAdd, isExp: false, mxF: numberAssociatedWithTextFieldToAdd))
self.tableView.reloadData()
}
I used an example definition of the addField method but you can change it to anything that you would like, just make sure that you change the data array and reload the table view data.
Lastly, we must define the delegate in the ListCell class. So add this line to the ListCell class:
weak var delegate: MyCustomCellDelegate?
You can then add the text field by running the following anywhere in your ListCell class:
delegate?.addField(titleOfTextFieldToAdd: "a name", numberAssociatedWithTextFieldToAdd: 50)
For more information on delegation, look at the answer to this question.
You have to append another item in your data array on button click and reload the tableview.
I want to show data in NSTableView. The number of columns is unspecified (could be any from 1 to ?? depending on the data), so I can't use Interface Builder.
So I initialize (with IB) my table to 1 column, and thereafter add new columns as required (then remove the no-longer needed 0-th column). To each added column I provide a unique identifier. So far so good.
I implement the tableView(-:viewForTableColumn:row) function, as shown below, but the makeViewWithIdentifier returns nil. What's the matter ?
If I detect the nil return, I create an instance of NSTableCellView with the proper identifier. But the data do not show in the table. What could be wrong ?
The code is below (with unnecessary lines removed) :
import Cocoa
class MyViewController: NSViewController {
#IBOutlet weak var dataTable: NSTableView!
}
var donnees: DataFrame = DataFrame() // Some table-like data with unspecified number of columns
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
initData() // get the actual data
// Make the ad-hoc number of columns
if donnees.nbCol > 0 {
for k in 0..<donnees.nbCol {
let newColumn = NSTableColumn(identifier: idArray[k]) // idArray : [String] of unique identifiers
dataTable.addTableColumn(newColumn)
}
}
dataTable.removeTableColumn(dataTable.tableColumns[0]) // remove the original column, now unnecessary
}
}
extension MyViewController : NSTableViewDataSource {
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return self.donnees.nbRow
}
}
extension MyViewController : NSTableViewDelegate {
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let columnID = tableColumn!.identifier
var cellView: NSTableCellView
let cellViewTmp = tableView.makeViewWithIdentifier(columnID, owner: self)
if cellViewTmp == nil { // then create a new NSTableCellView instance
cellView = NSTableCellView(frame: NSRect(x: 0, y: 0, width: (tableColumn?.width)!, height: 20))
cellView.identifier = columnID
print("CellView créé pour id \(columnID) au rang \(row)")
} else {
cellView = cellViewTmp as! NSTableCellView
}
cellView.textField?.stringValue = "AAA"
return cellView
}
}
Bingo ! Thanks to Willeke I rewrote my code as follows :
var donnees: DataFrame = DataFrame() // Some table-like data with unspecified number of columns
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
initData() // get the actual data
self.setTableColumns() // and prepare the columns accordingly
}
func setTableColumns() {
// In Interface Builder I've prepared a cell view in the 0-th tableColumn and set the identifier of this NSTableColumn to "ModelCellView"
let myCellViewNib = dataTable.registeredNibsByIdentifier!["ModelCellView"] // I save the cellView's Nib
// Make the ad-hoc number of columns
if donnees.nbCol > 0 {
for k in 0..<donnees.nbCol {
let newColumn = NSTableColumn(identifier: idArray[k]) // idArray : [String] of unique identifiers
dataTable.addTableColumn(newColumn)
dataTable.registerNib(myCellViewNib, forIdentifier: newColumn.identifier) // I register the above Nib for the newly added tableColumn
}
dataTable.removeTableColumn(dataTable.tableColumns[0]) // remove the original column, now unnecessary
}
}
extension MyViewController : NSTableViewDataSource {
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return self.donnees.nbRow
}
extension MyViewController : NSTableViewDelegate {
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let columnID = tableColumn!.identifier
let cellView = tableView.makeViewWithIdentifier(columnID, owner: self) as! NSTableCellView
cellView.textField?.stringValue = "theActualCellData"
return cellView
}
And it works perfectly as intended. Again, thanks to Willeke.
I have made a simple demo using TableView here: https://github.com/deadcoder0904/TableViewDemo
I have used Defaults module as a dependency
My project looks like
All the code is in ViewController.swift as follows -
import Cocoa
import Defaults
extension Defaults.Keys {
static let dreams = Defaults.Key<Array<String>>("dreams", default: [
"Hit the gym",
"Run daily",
"Become a millionaire",
"Become a better programmer",
"Achieve your dreams"
])
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var table: NSTableView!
var dreams = defaults[.dreams]
var selectedRow:Int = 0
override func viewDidLoad() {
super.viewDidLoad()
table.dataSource = self
table.delegate = self
}
override var acceptsFirstResponder : Bool {
return true
}
override func keyDown(with theEvent: NSEvent) {
if theEvent.keyCode == 51 {
removeDream()
}
}
func tableViewSelectionDidChange(_ notification: Notification) {
let table = notification.object as! NSTableView
selectedRow = table.selectedRow
}
func numberOfRows(in tableView: NSTableView) -> Int {
return dreams.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let dream = table.makeView(withIdentifier: tableColumn!.identifier, owner: self) as! NSTableCellView
dream.textField?.stringValue = dreams[row]
return dream
}
#IBAction func addTableRow(_ sender: Any) {
addNewDream()
}
#IBAction func removeTableRow(_ sender: Any) {
removeDream()
}
func addNewDream() {
dreams.append("Double Click or Press Enter to Add Item")
table.beginUpdates()
let last = dreams.count - 1
table.insertRows(at: IndexSet(integer: last), withAnimation: .effectFade)
table.scrollRowToVisible(last)
table.selectRowIndexes([last], byExtendingSelection: false)
table.endUpdates()
saveDreams()
}
func removeDream() {
if selectedRow >= dreams.count {
selectedRow = dreams.count - 1
}
if selectedRow != -1 {
dreams.remove(at: selectedRow)
table.removeRows(at: IndexSet(integer: selectedRow), withAnimation: .effectFade)
}
saveDreams()
}
func saveDreams() {
defaults[.dreams] = dreams
}
}
I want to do 2 things -
Get notified after Text Cell is edited so that I can save the changed data using Defaults module
After adding new Data by Clicking on the plus sign it adds Double Click or Press Enter to Add Item but what I want is I want to add Empty String which I can do with "" but I also want it to be focused & be editable so user can start entering text in it without having to Double Click or Press Enter.
I also want a solution in Swift 4 & not Objective-C. How to achieve this?
Use Cocoa Bindings, it's very powerful and saves a lot of boilerplate code.
Short tutorial:
Edit: To take full advantage of KVC the data source must be an NSObject subclass with dynamic properties
Create a simple class Dream (the description property is optional)
class Dream : NSObject {
#objc dynamic var name : String
init(name : String) { self.name = name }
override var description : String { return "Dream " + name }
}
In the view controller declare the data source array
var dreams = [Dream]()
and replace var selectedRow:Int = 0 with
#objc dynamic var selectedIndexes = IndexSet()
Go to Interface Builder
Select the table view, press ⌥⌘7 to go to the Bindings Inspector.
Bind Selection Indexes to View Controller Model Key Path selectedIndexes.
Press ⌥⌘6 and connect the dataSource (by drag&drop) to the view controller () .
Select the text field File 1 in Table Cell View in the table column. The easiest way is to ⌃⇧click in the text field area.
Press ⌥⌘7 and bind Value to Table Cell View Model Key Path objectValue.name (!)
In the view controller populate the data source array in viewDidLoad ( I don't know that framework so I leave it out) and reload the table view.
override func viewDidLoad() {
super.viewDidLoad()
let dreamNames = ["Hit the gym", "Run daily", "Become a millionaire", "Become a better programmer", "Achieve your dreams"]
dreams = dreamNames.map{Dream(name: $0)}
table.reloadData()
}
Delete acceptsFirstResponder
Delete tableViewSelectionDidChange
Delete tableView:viewFor:row:
Add
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return dreams[row]
}
Replace addNewDream with
func addNewDream() {
let last = dreams.count
dreams.append(Dream(name: "Double Click or Press Enter to Add Item"))
table.insertRows(at: IndexSet(integer: last), withAnimation: .effectGap)
table.scrollRowToVisible(last)
table.selectRowIndexes([last], byExtendingSelection: false)
saveDreams()
}
Replace removeDream() with
func removeDream() {
guard let selectedRow = selectedIndexes.first else { return }
dreams.remove(at: selectedRow)
table.removeRows(at: IndexSet(integer: selectedRow), withAnimation: .effectFade)
saveDreams()
}
To save the array when the text was edited afterwards you have to implement the delegate method controlTextDidEndEditing(_:)
override func controlTextDidEndEditing(_ obj: Notification) {
saveDreams()
}
and in Interface Builder connect the delegate of the text field in the table view to the view controller.
I think my problem spans over multiple VC.
In the table VC I perform a fetch request that returns an array of Floor objects. This entity has 2 attributes (floor number and number of rooms in floors). If I assign 2 floors (0 and 1) it works and prints them. The next VC contains a picker that displays the number of floors and a text box that is used to assign the number of rooms per floor. There must be a problem with this function because it gets called only if the first item of the picker is selected (prints the result). If I have 2 floors (0 and 1 in the picker) and floor 1 is selected the function does not assign the room value to any other floor. It doesn't matter how many floors, the function will only work for the first one. I have looked of how to modify the function but did not find any suitable solutions.
The second problem is that the table view does only display one row. Lets say that for floor 0 I assign 1; than only a row with 1 appears. If anyone could help me it would mean a lot to me. Thank you
Please see the code below for the picker function:
#IBAction func setTheFloors(_ sender: UIButton) {
if storedFloors.count > 0 {
if storedFloors.first?.floorNumber == pickedFloor! {
storedFloors.first?.numberOfRooms = roomNumberValue
print("\(storedFloors.first?.floorNumber) + \(storedFloors.first?.numberOfRooms)")
}
}
do {
try managedObjectContext.save()
} catch {
fatalError("could not save context because: \(error)")
}
}
#IBAction func nextStep(_ sender: UIButton) {
}
private func loadFloorData() {
let floorRequest: NSFetchRequest<Floors> = Floors.fetchRequest()
do {
storedFloors = try managedObjectContext.fetch(floorRequest)
} catch {
print("could not load data from core \(error.localizedDescription)")
}
}
private func spinnerItems() {
for i in 0...floorValue! - 1 {
convertedFloorValues.append(String(i))
}
}
and this section of code is for the table view.
class RoomAndAlarmTypeTableVC: UITableViewController, NSFetchedResultsControllerDelegate {
//MARK: - Properties
private var managedObjectContext: NSManagedObjectContext!
private var storedFloors = [Floors]()
private var floorsAndRooms = [String: String]()
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
loadFloorData()
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private func loadFloorData() {
let floorRequest: NSFetchRequest<Floors> = Floors.fetchRequest()
do {
storedFloors = try managedObjectContext.fetch(floorRequest)
print("\(storedFloors)")
} catch {
print("could not load data from core \(error.localizedDescription)")
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return storedFloors.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let specificFloor = storedFloors[section]
return Int(specificFloor.numberOfRooms)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "house cell", for: indexPath) as! ViewRooomNumberCell
let tableSections = storedFloors[indexPath.section]
let floorItem = tableSections.numberOfRooms[indexPath.row]
let floorNumber = String(floorItem.numberOfRooms)
cell.floorNumberTxt.text = floorNumber
return cell
}
}
I've been following "iOS8 Swift Programming Cookbook"'s section on EventKit and calendars, and I've learned a lot (especially since I'm new to programming). But the next step that I want to take is populating a tableview that I have an outlet to in my ViewController with the event data so as to have a tableview list of upcoming events. Can anyone tell me how to do this?
Here's what I have so far:
import UIKit
import EventKit
import EventKitUI
class TodayViewController: UIViewController, UITableViewDataSource {
var events: AnyObject = []
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
requestCalendarAccess()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func requestCalendarAccess() {
let eventStore = EKEventStore()
switch EKEventStore.authorizationStatusForEntityType(EKEntityTypeEvent){
case .Authorized:
readEvents()
case .Denied:
displayAccessDenied()
case .NotDetermined:
eventStore.requestAccessToEntityType(EKEntityTypeEvent, completion:
{[weak self] (granted: Bool, error: NSError!) -> Void in
if granted{
self!.readEvents()
} else {
self!.displayAccessDenied()
}
})
case .Restricted:
displayAccessRestricted()
}
}
func displayAccessDenied(){
println("Access to the event store is denied.")
}
func displayAccessRestricted(){
println("Access to the event store is restricted.")
}
func readEvents(){
/* Instantiate the event store */
let eventStore = EKEventStore()
let icloudSource = sourceInEventStore(eventStore,
type: EKSourceTypeCalDAV,
title: "iCloud")
if icloudSource == nil{
println("You have not configured iCloud for your device.")
return
}
let calendar = calendarWithTitle("Work",
type: EKCalendarTypeCalDAV,
source: icloudSource!,
eventType: EKEntityTypeEvent)
if calendar == nil{
println("Could not find the calendar we were looking for.")
return
}
/* The event starts from today, right now */
let startDate = NSDate()
/* The end date will be 1 day from today */
let endDate = startDate.dateByAddingTimeInterval(24 * 60 * 60)
/* Create the predicate that we can later pass to the
event store in order to fetch the events */
let searchPredicate = eventStore.predicateForEventsWithStartDate(
startDate,
endDate: endDate,
calendars: [calendar!])
/* Fetch all the events that fall between
the starting and the ending dates */
let events = eventStore.eventsMatchingPredicate(searchPredicate)
as [EKEvent]
if events.count == 0 {
println("No events could be found")
} else {
// Go through all the events and print them to the console
for event in events{
println("Event title = \(event.title)")
println("Event start date = \(event.startDate)")
println("Event end date = \(event.endDate)")
}
}
}
func sourceInEventStore(
eventStore: EKEventStore,
type: EKSourceType,
title: String) -> EKSource?{
for source in eventStore.sources() as [EKSource]{
if source.sourceType.value == type.value &&
source.title.caseInsensitiveCompare(title) ==
NSComparisonResult.OrderedSame{
return source
}
}
return nil
}
func calendarWithTitle(
title: String,
type: EKCalendarType,
source: EKSource,
eventType: EKEntityType) -> EKCalendar?{
for calendar in source.calendarsForEntityType(eventType).allObjects
as [EKCalendar]{
if calendar.title.caseInsensitiveCompare(title) ==
NSComparisonResult.OrderedSame &&
calendar.type.value == type.value{
return calendar
}
}
return nil
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return events.count
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier("cell")
as UITableViewCell
cell.textLabel!.text = /*what goes here?*/
return cell
}
Right now my events are printing to the console perfectly, but I'm not sure how to get from there to the tableview in my view controller. Any help is appreciated!
at first your event data should be accessible in you table delegate
func readEvents(){
...
let events = eventStore.eventsMatchingPredicate(searchPredicate)
as [EKEvent]
...
}
but events are NOT !!!
you fetch your data just locally in you function readEvents
eventhough you declare a store for events in your class, you never filled it
class TodayViewController: UIViewController, UITableViewDataSource {
var events: AnyObject = []
...
}
to fill the data in you class variable just remove 'redeclaration'
...
var events: [EKEvent] = []
func readEvents(){
...
events = eventStore.eventsMatchingPredicate(searchPredicate)
as [EKEvent]
...
}
The function
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell
gets passed the parameter cellForRowAtIndexPath which contains the index of the row inside your tableview. You can access that index using indexPath.row, which you should use to access your events-Array.
So for example your function could look like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
var event = self.events[indexPath.row]
cell.textLabel!.text = event.eventIdentifier
return cell
}
I don't know exactly how the EKEvent Class looks like, but Apple's Documentation of that class says there is the eventIdentifier of type String and so you can take that to test it with your tableview.
Hope you enjoy programming! :)