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

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?

Related

Store value in UIStepper class

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
}
}
}

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.

Set object properties using a UISwitch in a custom cell

I'm trying to do something that should be very simple, but I'm having issues due to my inexperience with Swift.
I have a ViewController that has a TableView inside of it with custom cells that are populated from an array of objects (called allListItems). These objects were created using Realm Model Object, which I'm using instead of Core Data, which I think might be pertinent. Each custom cell has a UISwitch in it, and ideally I'd like to set it up so that when the user toggles the UISwitch, it modifies the boolean isSelected property for that indexPath.row, and then appends that object to a separate array, called selectedListItems.
All of my searching through SO, Tuts+, and AppCoda has revealed that I should be using a protocol - delegate pattern here, with my protocol in my custom cell class and my delegate in my ViewController class. After flailing away at it for most of the day I haven't had any luck, however, which I think might be due to the arrays being Realm Model Objects.
As I mentioned, I'm very new to Swift and programming in general, so ELI5 responses are much appreciated! Thanks in advance!
For reference, here is my custom cell:
import UIKit
class AllListItemsTableViewCell: UITableViewCell {
#IBOutlet var toggleIsSelected: UISwitch!
#IBOutlet var listItemLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Instead of the suggested protocol / delegate pattern use a callback.
This is very easy in Swift.
In the table view cell declare a optional variable with a closure
var callback : ((UITableViewCell, Bool) -> Void)?
and call it in the IBAction for the switch
#IBAction func switchChanged(sender : UISwitch) {
callback?(self, sender.on)
}
In cellForRowAtIndexPath set the callback
cell.callback = { (tableViewCell, switchState) in
if let indexPath = self.tableView.indexPathForCell(tableViewCell) {
// do something with index path and switch state
}
}
To pass the cell back can be useful if the cell was moved meanwhile to get the most recent index path.

Change #IBOutlet from a subview

I'm trying to enable or disable an #IBOutlet UIButton Item of a toolbar from a UIView.
The button should get disabled when the array that I'm using in EraseView.Swift is empty.
I tried creating an instance of the view controller but it gives me the error (found nil while unwrapping):
in EraseView:
class EraseView: UIView {
...
let editViewController = EditImageViewController()
//array has item
editViewController.undoEraseButton.enabled = true //here I get the error
...
}
I tried to put a global Bool that changed the value using it in EditImageViewController but it doesn't work:
var enableUndoButton = false
class EditImageViewController: UIViewController {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
viewDidLoad() {
undoEraseButton.enabled = enableUndoButton
}
}
class EraseView: UIView {
...
//array has item
enableUndoButton = true //here I get the error
...
}
I know it's simple but I can't let it work. Here's the situation:
The root of the problem is the line that says:
let editViewController = EditImageViewController()
The EditImageViewController() says "ignore what the storyboard has already instantiated for me, but rather instantiate another view controller with no outlets hooked up and use that." Clearly, that's not what you want.
You need to provide some way for the EraseView to inform the existing view controller whether there was some change to its "is empty" state. And, ideally, you want to do this in a way that keeps these two classes loosely coupled. The EraseView should only be informing the view controller of the change of the "is empty" state, and the view controller should initiate the updating of the other subviews (i.e. the button). A view really shouldn't be updating another view's outlets.
There are two ways you might do that:
Closure:
You can give the EraseView a optional closure that it will call when it toggles from "empty" and "not empty":
var emptyStateChanged: ((Bool) -> ())?
Then it can call this when the state changes. E.g., when you delete the last item in the view, the EraseView can call that closure:
emptyStateChanged?(true)
Finally, for that to actually do anything, the view controller should supply the actual closure to enable and disable the button upon the state change:
override func viewDidLoad() {
super.viewDidLoad()
eraseView.emptyStateChanged = { [unowned self] isEmpty in
self.undoEraseButton.enabled = !isEmpty
}
}
Note, I used unowned to avoid strong reference cycle.
Delegate-protocol pattern:
So you might define a protocol to do that:
protocol EraseViewDelegate : class {
func eraseViewIsEmpty(empty: Bool)
}
Then give the EraseView a delegate property:
weak var delegate: EraseViewDelegate?
Note, that's weak to avoid strong reference cycles. (And that's also why I defined the protocol to be a class protocol, so that I could make it weak here.)
The EraseView would then call this delegate when the the view's "is empty" status changes. For example, when it becomes empty, it would inform its delegate accordingly:
delegate?.eraseViewIsEmpty(true)
Then, again, for this all to work, the view controller should (a) declare that is conforms to the protocol; (b) specify itself as the delegate of the EraseView; and (c) implement the eraseViewIsEmpty method, e.g.:
class EditImageViewController: UIViewController, EraseViewDelegate {
#IBOutlet weak var undoEraseButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
eraseView.delegate = self
}
func eraseViewIsEmpty(empty: Bool) {
undoEraseButton.enabled = !empty
}
}
Both of these patterns keep the two classes loosely coupled, but allow the EraseView to inform its view controller of some event. It also eliminates the need for any global.
There are other approaches that could solve this problem, too, (e.g. notifications, KVN, etc.) but hopefully this illustrates the basic idea. Views should inform their view controller of any key events, and the view controller should take care of the updating of the other views.

Singleton in swift is not accessible from another class

I am new to Swift and am trying to access a 'problemSolved' array that is appended during gameplay in my main GameController class, from another class. For some reason the array is not visible in a UIViewController class where I want to show all the problems solved in a table. I have read many of the Singleton examples on the site to see if this will do it, but that doesn't seem to. Any help or advice here much appreciated!
class GameController: TileDragDelegateProtocol, CenterViewControllerDelegate {
static let sharedInstance = GameController()
var problemsSolved = Array<String>()
func onProblemSolved() {
problemsSolved.append(problem)
println("problemsSolved contains \(problemsSolved)")
}
}
During gameplay I can see in the console that the array is being appended ok in GameController. But when I try to access it in a ViewController class the contents are showing as empty [].
class SidePanelViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
tableView.reloadData()
println("the array viewed from here is empty \(GameController.sharedInstance.problemsSolved)")
}
At the moment I only can imagine that you don't call
GameController.sharedInstance.onProblemSolved()
when you want to append a String to problemsSolved.
You should also consider making your functions and variables in GameController static.
If this doesn't solve your problem I would need more information about how and when you add something to problemsSolved.