Store value in UIStepper class - swift5

I have stepper in my UITableViewCells.
I saw from other answers that people are using UIStepper.tag to pass the indexPath.row , but I have sections in my UITableView and I need to save the indexPath directly in the class UIStepper.
extension UIStepper {
struct Save {
static var indexPath:IndexPath?
}
public var indexPath:IndexPath {
get{
return Save.indexPath!
}
set(newValue) {
Save.indexPath = newValue
}
}
}
I'm using this code to store the indexPath. In my cellForRow I set
stepper.indexPath = indexPath, but my indexPath for the UIStepper is always the last one.
Every UIStepper have the last indexPath.
If I have 4 rows, the output UIStepper.indexPath.row is always 3 for all cells.
How to fix that?

I understand what you're trying to do. I don't know if it's the best solution but the problem you're having is caused because the property is static for the whole class, so when you set the value for whatever row, what you had before gets overwritten.
When you load the first cell with cellForRowAt, you set the indexPath to 0-0. For the second row, you set it to 0-1, and so on. The last row sets it to whatever value it has at that moment and whatever you had before gets lost.
In other words, that value is shared for all instances (it's a class property).
What you need is an instance property so each object has its own memory for that value. Instead of using an extension, you could create a UIStepper subclass that only adds an indexPath property to its definition and use that instead. Something like:
class CellStepper: UIStepper {
var indexPath: IndexPath?
}
Then, in cellForRowAt set it to the value you need.
I suppose you're setting the same method as target for valueChanged to each stepper and when that gets called, you could use the sender to cast it to CellStepper and access the indexPath property to know what row's stepper changed.
If you'd like sample code, I can elaborate.

What you try to do in extension UIStepper is bad.
Disclaimer: What I propose below is also bad, even worse. If you can, avoid this approach and use inheritance as proposed by #George_Alegre - this is the best and correct way.
But... if for some very very strange reason you cannot use subclassing it is possible to make what you did operable. The main issue is in static - it is shared between all instances of a class, that is why all your steppers have the latest set value. So, let's just replace one value with container which will hold pairs of reference to instance and desired value.
IMPORTANT: YOU MUST CLEAN THAT CONTAINER AFTER WORK WITH THIS WORKFLOW
eg. in deinit of controller that manages this table
Here is approach:
extension UIStepper {
struct Save {
static var indexPaths = [UIStepper: IndexPath]()
// !!! MUST BE CALLED AT THE END OF USAGE (eg. in controller deinit)
static func cleanup() {
indexPaths = [:]
}
}
public var indexPath: IndexPath {
get {
return Save.indexPaths[self] ?? IndexPath()
}
set(newValue) {
Save.indexPaths[self] = newValue
}
}
}

Related

How do I get UIConfigurationState without a reference to the cell?

Normally, when updating a cell's contentConfiguration for a particular cell's state you ask the cell for its contentConfiguration, then update it using updated(for:).
let content = cell.defaultContentConfiguration().updated(for: cell.configurationState)
However, in order to get this state you first need to have a reference to the cell. UIConfigurationState doesn't have an initializer. How can get the updated styling for a state without a reference to the cell?
For example, here I am trying to create a reusable configuration that adjusts itself for particular state
class Person {
let name: String
}
extension Person {
func listContentConfig(state: UICellConfigurationState) -> UIListContentConfiguration {
var content = UIListContentConfiguration.cell().updated(for: state)
content.text = self.name
return content
}
}
Then, during cell registration I can configure it with my reusable config.
extension UICollectionViewController {
func personCellRegistration(person: Person) -> UICollectionView.CellRegistration<UICollectionViewListCell, Person> {
return .init { cell, indexPath, person in
cell.contentConfiguration = person.listContentConfig(state: cell.configurationState)
}
}
}
That works fine, but what if I want to mix and match different properties for difference states? In order to actually get this state I need to first get the cell, update the state, then set it back. This is quite a few steps.
extension UICollectionViewController {
func personCellRegistration(person: Person) -> UICollectionView.CellRegistration<UICollectionViewListCell, Person> {
return .init { cell, indexPath, person in
// 1. Change the cell's state
cell.isUserInteractionEnabled = false
// 2. Grab my content config for the new state
let disabledConfig = person.listContentConfig(state: cell.configurationState)
// 3. Change the cell's state back
cell.isUserInteractionEnabled = true
// 4. Get the cell's default config
var defaultConfig = cell.defaultContentConfiguration()
// 5. Copy the pieces I want
defaultConfig.textProperties.color = disabledConfig.textProperties.color
}
}
}
What I'd like is to be able to do something like this:
extension Person {
func listContentConfig(state: UICellConfigurationState) -> UIListContentConfiguration {
let disabledState = UICellConfigurationState.disabled // no such property exists.
var content = UIListContentConfiguration.cell().updated(for: disabledState)
// customize...
}
}
I realize that I could pass in the cell itself to my reusable config, but this a) breaks encapsulation, b) defeats the purpose of configurations to be view agnostic, c) requires the same number of steps.
(FYI: The reason I am doing this is to allow the user to delete a cell that represents 'missing data'. The cell's style should appear disabled, but when setting isUserInteractionEnabled = false the delete accessory becomes unresponsive.)
Am I missing something?

Storing a weak reference to a UITableView cell without causing memory leaks

I think I'm understanding the weak keyword properly, but wanted to quickly sense check that I'm not doing something wrong.
I'm wanting to store a reference to a specific UITableViewCell so that I can call functions or edit values when needed. Now I don't need to be able to edit the values all the time, only when it's on screen so I don't need to keep a hard copy of the cell at all times. What I've done is as follows:
class customTableViewCell: UITableViewCell {
var variable1: String?
func doAThing(index: Int) {
self.variable1 = "did a thing"
}
}
class MainView: UIView {
weak var cellToManipulate: CustomTableViewCell?
func manipulateCell() {
self.cellToManipulate?.doAThing(index: 1)
}
}
Obviously there will be a tableview and everything else involved, but if I store a weak reference to a cell, it will be removed from memory and set to nil when the tableview scrolls right?
Is there maybe a better way to go about this, aside from forEaching through each item in the visibleCells array?

Private IBOutlets Swift

I know that our IBOutlets should be private, but for example if I have IBOutlets in TableViewCell, how should I access them from another ViewController? Here is the example why I'm asking this kind of question:
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitle: UILabel!
}
if I assign to the IBOutlet that it should be private, I got an error in another ViewController while I'm accessing the cell property: 'bookTitle' is inaccessible due to 'private' protection level
If I understand your question correctly, you are supposing the #IBOutlet properties should be marked as private all the time... Well it's not true. But also accessing the properties directly is not safe at all. You see the ViewControllers, TableViewCells and these objects use Implicit unwrapping on optional IBOutlets for reason... You don't need to init ViewController when using storyboards or just when using them somewhere in code... The other way - just imagine you are creating VC programmatically and you are passing all the labels to the initializer... It would blow your head... Instead of this, you come with this in storyboard:
#IBOutlet var myLabel: UILabel!
this is cool, you don't need to have that on init, it will just be there waiting to be set somewhere before accessing it's value... Interface builder will handle for you the initialization just before ViewDidLoad, so the label won't be nil after that time... again before AwakeFromNib method goes in the UITableViewCell subclass, when you would try to access your bookTitle label property, it would crash since it would be nil... This is the tricky part about why this should be private... Otherwise when you know that the VC is 100% on the scene allocated there's no need to be shy and make everything private...
When you for example work in prepare(for segue:) method, you SHOULD NEVER ACCESS THE #IBOutlets. Since they are not allocated and even if they were, they would get overwritten by some internal calls in push/present/ whatever functions...
Okay that's cool.. so what to do now?
When using UITableViewCell subclass, you can safely access the IBOutlets (ONLY IF YOU USE STORYBOARD AND THE CELL IS WITHIN YOUR TABLEVIEW❗️)
and change their values... you see
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// We shouldn't return just some constructor with UITableViewCell, but who cares for this purposes...
guard let cell = tableView.dequeueReusableCell(withIdentifier: "bookTableViewCell", for: indexPath) else { return UITableViewCell() }
cell.bookTitle.text = "any given text" // This should work ok because of interface builder...
}
The above case should work in MVC pattern, not MVVM or other patterns where you don't use storyboards with tableViewControllers and embed cells too much... (because of registering cells, but that's other article...)
I will give you few pointers, how you can setup the values in the cell/ViewController without touching the actual values and make this safe... Also good practice (safety) is to make the IBOutlets optional to be 100% Safe, but it's not necessary and honestly it would be strange approach to this problem:
ViewControllers:
class SomeVC: UIViewController {
// This solution should be effective when those labels could be marked weak too...
// Always access weak variables NOT DIRECTLY but with safe unwrap...
#IBOutlet var titleLabel: UILabel?
#IBOutlet var subtitleLabel: UILabel?
var myCustomTitle: String?
var myCustomSubtitle: String?
func setup(with dataSource: SomeVCDataSource ) {
guard let titleLabel = titleLabel, let subtitleLabel = subtitleLabel else { return }
// Now the values are safely unwrapped and nothing can crash...
titleLabel.text = dataSource.title
subtitleLabel.text = dataSource.subtitle
}
// WHen using prepare for segue, use this:
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = myCustomTitle
subtitleLabel.text = myCustomSubtitle
}
}
struct SomeVCDataSource {
var title: String
var subtitle: String
}
The next problem could be this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? SomeVC else { return }
let datasource = SomeVCDataSource(title: "Foo", subtitle: "Bar")
// This sets up cool labels... but the labels are Nil before the segue occurs and even after that, so the guard in setup(with dataSource:) will fail and return...
destinationVC.setup(with: datasource)
// So instead of this you should set the properties myCustomTitle and myCustomSubtitle to values you want and then in viewDidLoad set the values
destinationVC.myCustomTitle = "Foo"
destinationVC.myCustomSubtitle = "Bar"
}
You see, you don' need to set your IBOutlets to private since you never know how you will use them If you need any more examples or something is not clear to you, ask as you want... Wish you happy coding and deep learning!
You should expose only what you need.
For example you can set and get only the text property in the cell.
class BookTableViewCell: UITableViewCell {
#IBOutlet weak private var bookTitleLabel: UILabel!
var bookTitle: String? {
set {
bookTitleLabel.text = newValue
}
get {
return bookTitleLabel.text
}
}
}
And then, wherever you need:
cell.bookTitle = "It"
Now outer objects do not have access to bookTitleLabel but are able to change it's text content.
What i usually do is configure method which receives data object and privately sets all it's outlets features.
I haven't come across making IBOutlets private to be common, for cells at least. If you want to do so, provide a configure method within your cell that is not private, which you can pass values to, that you want to assign to your outlets. The function within your cell could look like this:
func configure(with bookTitle: String) {
bookTitle.text = bookTitle
}
EDIT: Such a function can be useful for the future, when you change your cell and add new outlets. You can then add parameters to your configure function to handle those. You will get compiler errors everywhere, where you use that function, which allows you to setup your cell correctly wherever you use it. That is helpful in a big project that reuses cells in different places.

Using swift to populate NSTableView rows with a NSPopupButtonCell

I have been trying to change one of the cells in an NSTableView to a pull-down menu, but have been unsuccessful. I read the Apple developer documentation, but it doesn't give an example of how to use NSPopupButtonCell in a NSTableView. I searched forums, including here, and only found one somewhat relevant example, except that it was in objective-c, so it doesn't work for my swift app. Code for the table is here:
extension DeviceListViewController:NSTableViewDataSource, NSTableViewDelegate{
// get the number of rows for the table
func numberOfRows(in tableView: NSTableView) -> Int {
return homedevices.count
}
// use the data in the homedevices array to populate the table cells
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?{
let result = tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as! NSTableCellView
if tableColumn?.identifier == "ID" {
result.textField?.stringValue = homedevices[row].id
} else if tableColumn?.identifier == "Name" {
result.textField?.stringValue = homedevices[row].name
result.imageView?.image = homedevices[row].image
} else if tableColumn?.identifier == "Type" {
result.textField?.stringValue = homedevices[row].type
} else if tableColumn?.identifier == "Button" {
result.textField?.integerValue = homedevices[row].button
}
return result
}
// facilitates data sorting for the table columns
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let dataArrayMutable = NSMutableArray(array: homedevices)
dataArrayMutable.sort(using: tableView.sortDescriptors)
homedevices = dataArrayMutable as! [HomeDevice]
tableView.reloadData()
}
}
I really just want to be able to allow pull-down selection to change the button assigned to a particular homedevice (a simple integer), instead of having to type a number into the textfield to edit this value. Unfortuantely, when I add the popupbuttoncell to my table in IB, all of the views for my table cells are removed. So I may need to create the table differently. But most of the things I have read about and tried have caused runtime errors or display an empty table.
EDIT:
Day 3:
Today I have been reading here: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TableView/PopulatingViewTablesWithBindings/PopulatingView-TablesWithBindings.html
and many other places too, but I don't have rep to post any more links.
I have added a NSPopupButton in IB, but am not sure how to set the value. I tried result.objectValue = homedevices[row].button, but that does not work. I suppose that I need an array controller object. So then I tried creating an outlet for the object in my DeviceListViewController like #IBOutlet var buttonArrayController: NSArrayController! I guess that I now need to somehow find a way to connect the array controller to my homedevices array.
so I looked at example code here:
https://github.com/blishen/TableViewPopup
This is in objective-C, which is not a language I am using, but maybe if I keep looking at it at various times over the course of the week, I might figure out how to make a pull-down menu.
So I am continuing to work at this, with no solution currently.
This issue is solved, thanks to #vadian.
The button is inserted as NSPopUpButton object, rather than a NSPopUpButtonCell.
Then the cell gets its own custom class, which I called ButtonCellView as a subclass of NSTableCellView.
Then the created subclass can receive an outlet from the NSPopUpButton to the custom subclass. I can give this a selectedItem variable and create the menu here.
Then in the table view delegate, when making the table, I can just set the selectedItem of my ButtonCellView object to the value from my data array.
It works great!

Segue in UITableView with multiple sections, each containing objects filtered from a single array

I'm a beginner, clearly out of my league and I haven't been able to find an answer online.
I have a UITableViewController with a UITableViewshowing custom objects stored in one array. I don't show all the object of the array in one single section of said TableView: the TableView has multiple sections, each containing a filtered portion of my objects array (I filter the custom objects array checking that the object category property is equal to a category that I specified in a categories array).
This filtering and showing the single array in different sections is working fine (I understand that maybe it's not elegant, as I said I'm a beginner in coding and I absolutely needed to work with one single array, without creating other arrays corresponding to the filtered results), but to better understand my issue I think it's better that I show what I did, so here's the TableView part of my code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return myCategoriesArray.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
for (var i = 0; i <= section; i++){
if section == i {
for eachCategory in myCategoriesArray {
return myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }.count
}
}
}
// ...
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("objectCell", forIndexPath: indexPath) as UITableViewCell
for (var i = 0; i <= indexPath.section; i++){
if indexPath.section == i {
for eachCategory in myCategoriesArray {
cell.textLabel?.text = myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }[indexPath.row].nameProperty
return cell
}
}
}
// ...
}
This works in the sense that I have the UITableViewController showing all my objects, but filtered in separated sections by category.
My issue is with the segue when I select a cell and show a detail view.
Here's my prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var nextVC = segue.destinationViewController as MyNextViewController
if let indexPath = tableView.indexPathForSelectedRow() {
let selected = myObjectsArray![indexPath.row]
nextVC.passedObject = selected
}
}
}
I'm sure that many of you already see my issue: the object that I pass to the next ViewController is selected in the custom objects array using as index [indexPath.row], but indexPath.row starts at 0 for each section, so when I select an object its index in the TableView is not equal to the index in the custom objects array, meaning that I pass the wrong object.
Now, I'm stuck because I don't see a way to pass the right (meaning, selected) object to the next View Controller while preserving the fact that I'm working with only a single array.
I was toying with the idea of adding an objectIDString property to every object and a single var currentlySelectedObjectIDString that is set every time a cell is selected and try to pass to the next View Controller the object with the objectIDString property matching the currentlySelectedObjectIDString, but it looks like a bad idea to my inexperienced eyes and I'm actually not sure how I could accomplish that even if I wanted to (maybe implementing didSelectRowAtIndexPath:, but I have not been able to make it work).
Any help would be really appreciated, I've been stuck on this for so long I begin to question a) my sanity b)every decision I made so far in the project (meaning, the single array for all objects that is filtered in sections), but I'm already so invested in it that I really would like not to have to start over.
Thank you,
Cesare
P.S. I hope my question is clear, english isn't my main language... sorry for any mistake!
I suggest you, to use a NSFetchedResultsController. This class have a property sectionNameKeyPath. In this property you could set your category and you won't need more iterate with a repetition in each numberOfSection and numberOfRows.
like this:
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: "event.startDate", cacheName: nil)
In my case i need filter data by event.startDate.
I don't know if you are using core data, but if you are using, this is the better way to do this.
I'll expose them for you!
In the first moment NSFetchedResultsController like complicated, but its very very useful. Don't be afraid.
I don't know exactly your model and data. In this case i'll show you my owner sample.
Please see my question in the following link:
Sectioning TableView and rows with Core Data Swift
In this link, see my question, and in the bottom i'll explain the complete solution with the others answer.
If this is not clear for you, please, talk with me :.)
I spent all day trying to figure out a way to solve my own question above and I think I've finally found a working-workaround.
My premise and disclaimer is that this is a pile of hacks, I post this only in case this might help someone in my situation in the future, but clearly the way to deal with this kind of situation is Core Data, as suggested by Weles' answer, not what I did.
Here's briefly what I've done to get my multi-component UITableView, in which all the data come from a single array of custom objects that is filtered by a different value in every component, to pass the selected object to the detail view when a cell is selected.
1) I added to all my customObjects an objectID : String computed property (current date + random number).
2) I added a var currentlySelectedObjectID : String? in my TableViewController.
3) I subclassed UITableViewCell, creating a CustomTableViewCell class that only adds to the normal class a var selectedCellID : String?, then I changed my cellForRowAtIndexPath to return a CustomTableViewCell instead of a UITableViewCell. Inside this method, before returning the cell, I also set the property selectedCellID of the cell equal to objectID of the current object. I also had to change the class of the cell in the Storyboard from UITableViewCell to CustomTableViewCell.
4) In the Storyboard I removed the segue from the cell to the detailViewController that was automatically created by Xcode and I set a custom StoryboardID to the detailViewController ("detailVC"),
5) Inside didSelectRowAtIndexPath of TableViewController I did all the work that before I was trying to do in prepareForSegue, but in a different way (not a segue, a self.navigationController?.pushViewController). Here's the code:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow();
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as CustomTableViewCell!;
self.currentlySelectedObjectID = currentCell.selectedCellID
// detailViewController instance
var detailVC = self.storyboard?.instantiateViewControllerWithIdentifier("detailVC") as MyDetailViewController
// I filter my objects array to "extract" the object with the objectID property equal to the currentlySelectedObjectID property (which is equal to the currentCell.selectedCellID, as set above). This array must have only 1 value. If so, I set the property passedCustomObject that I have in my detailViewController to the same object selected.
if (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID }).count == 1 {
detailVC.passedCustomObject = (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID })[0]
} else {
println("Error passing the object selected in the TableView to the DetailView")
}
// I push the detailViewController on top of the stack
self.navigationController?.pushViewController(detailVC, animated: true)
}
I think there are very good chance that a decent programmer (I am not one, but I hope to become one some day), seeing what I did, could faint.
Again, I don't think anyone should do this, if you're in my same situation go straight to Core Data: I spent a day on this, there's good chance that in three or four I could have had Core Data working.
But still, as hacked and inefficient as this is, it works... I tested multiple times. So, having spent so much time and having found no useful similar previous answers online, I thought to post mine.
Don't do this, I'm really afraid this is easily breakable! :)
I still look forward to other answers, to learn from my numerous mistakes!