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

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?

Related

How can I automatically asynchronously set a new property of an existing struct after initialization?

A struct Item I am using has a property named path which contains a String of the location of an image. I'm trying to make it so that when an instance of Item is created, the image will automatically be fetched and set to a new image property.
Are extensions the way to go for this? I can't seem to figure out how to do it.
extension Item {
var image: UIImage {
if self.path == nil {
return UIImage(named: "default.png")!
}
ASYNC_CALL_HERE {
// need to do something with the image here
}
}
}
Or is there a better way?
Note: Item is not my struct.
First, I should point out that your attempt is trying to do something quite different from what you said you want to do in the first paragraph.
If we go by what you said in the first paragraph, you want a stored property that is initialised when Item is initialised. Unfortunately, you can't add stored properties in extensions, so you can't use an extension. Item is a struct, so you can't subclass it either. The only option I see is to create a wrapper class:
class ItemWrapper {
let item: Item
var image: UIImage?
init(<insert parameters of Item.init>) {
item = Item(<insert parameters of Item.init>)
if item.path == nil {
image = UIImage(named: "default.png")!
} else {
ASYNC_CALL_HERE { fetchedImage in
image = fetchedImage
}
}
}
}
However, if it doesn't have to be a stored property, we can use a similar approach as the one in your attempt. Rather than fetching the image immediately after Item is initialised, we only fetch it when the caller needs it. For this, we could use an extension to add a method called getImage. You might want to use a dictionary stored somewhere (or something else) as a cache, to simulate a stored property.
func getImage(completion: #escaping (UIImage) -> Void) {
guard let path = item.path else {
completion(UIImage(named: "default.png")!)
return
}
if let image = getImageFromCache(item.path!) {
completion(image)
return
}
ASYNC_CALL_HERE { fetchedImage in
completion(fetchedImage)
storeImageInCache(fetchedImage, path)
}
}

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

How to detect dismissed TextView in TableView?

I have a tableView with dynamic cells with multiple TextViews. I am dismissing a keyboard with a "Cancel" and trying to determine which TextView is being dismissed to "undo" the changes made by user.
Based on this similar question: How to determine which textfield is active swift I have adapted one of the answers for the following extension:
extension UIView {
var textViewsInView: [UITextView] {
return subviews
.filter ({ !($0 is UITextView) })
.reduce (( subviews.compactMap { $0 as? UITextView }), { summ, current in
return summ + current.textViewsInView
})
}
var selectedTextView: UITextView? {
return textViewsInView.filter { $0.isFirstResponder }.first
}
}
This is working and I am presently testing in the following code:
#objc func cancelButtonAction() {
if let test = tableView.selectedTextView {
print("View Found")
}
tableView.beginUpdates()
tableView.endUpdates()
}
Doing a break at print("View Found") I can inspect "test". The following is the result.
This appears to only identify the view Test by a memory address. My question is how do I interpret this to identify the view that was being edited?
Update: There seems to be some issue in understanding. Assume I have a table with two cells and in each cell two textViews (dynamic cells). Assume the table loads by saying in the 4 textViews. "Hi John", "Hi Sam", "Bye John", "Bye Sam". Suppose the user starts editing a cell and changes one cell to read "Nachos". Then the user decides to cancel. I want to then replace with the value that was there before (from my model). I can find the textView but it now reads "Nachos". Therefore I do not know which textView to reload with the appropriate Hi and Bye.
Implement a placeholder for your textviews so that when their text is empty, it will have a default value. Therefore, when a user presses cancel while in focus of a textview, we can set the textview's text to its default value. See link to implement a textview placeholder.. Text View Placeholder Swift
I did solve this problem by adding the .tag property to the textView objects. I also dropped the extension approach and used textView delegate. The solution required me to first assign the tag and delegate = self for each textView in the tableView: cellForRowAt. The following shows one TextView of many. Notice the tag is setup so I may determine the section and the row it came from and the specific item.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
...
cell.directionsTextView.delegate = self
cell.directionsTextView.tag = indexPath.section*1000 + indexPath.row+1
return cell
}
Two global variables are defined in my tableView class:
var activeTextView = UITextView()
var activeTextViewPresentText = String()
The textViewDidBeginEditing captures the original state of the textView text before the user starts editing.
// Assign the newly active textview to store original value and original text
func textViewDidBeginEditing(_ textView: UITextView) {
print("textView.tag: \(textView.tag)")
self.activeTextView = textView
self.activeTextViewPresentText = textView.text
}
Lastly, if the user cancels the editing, the original text is reloaded.
#objc func cancelButtonAction() {
if activeTextView.text != nil {
activeTextView.text = activeTextViewPresentText
}
self.view.endEditing(true)
tableUpdate()
}

How to filter table view cells with a UISegmentedControl in Swift?

I've already searched this before asking the question but I didn't find what I need.
I'm building this app where the user puts a task (not going to the app store, just for me and some friends), and the task has a category. For example: school, home, friends, etc. When the user is going to add a new task, there are 2 text fields, the description text field and the category text field. I'm using a UIPickerView so the user picks a category, then, after creating the new task, it will add the category to an array I've created called "categories".
I want to put an UISegmentedControl on top of the table view with the sections:
All - School - Home - Friends
If all is selected, it will show all the cells with no filtering. If not, it will show the cell(s) with the corresponding categories.
I've read that I need to create table view sections to each category, but this would change my code a lot, and I don't even have an idea of how to work with multiple table view sections, I've tried once but it kept repeating the cells of one section in the second.
So how can I filter the cells per category?
Can I just put for example this? :
if //code to check in which section the picker is here {
if let schoolCell = cell.categories[indexPath.row] == "School" {
schoolCell.hidden = true
}
}
Please help me!!!
EDIT:
I have this code by now:
if filterSegmentedControl.selectedSegmentIndex == 1 {
if categories[indexPath.row] == "School" {
}
}
I just don't know where to go from here. How do I recognize and hide the cells?
It seems to me that you may want to take a simpler approach first and get something working. Set up your ViewController and add a tableView and two(2) arrays for your table data. One would be for home and the other for work. Yes, I know this is simple but if you get it working, then you can build on it.
Add a variable to track which data you are displaying.
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var tableView: UITableView!
// You would set this to 0, 1 or 2 for home, work and all.
var dataFilter = 0
// Data for work tasks
var tableDataWork : [String] = ["Proposal", "Send mail", "Fix printer", "Send payroll", "Pay rent"]
// Data for home tasks
var tableDataHome : [String] = ["Car payment", "Mow lawn", "Carpet clean"]
Add these functions for the segmented control.
#IBAction func segmentedControlAction(sender: AnyObject) {
switch segmentedControl.selectedSegmentIndex {
case 0:
print("Home")
dataFilter = 0
case 1:
print("Work")
dataFilter = 1
case 2:
print("All")
dataFilter = 2
default:
print("All")
dataFilter = 2
}
reload()
}
func reload() {
dispatch_async( dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("task-cell")
var title: String?
switch dataFilter {
case 0:
title = tableDataHome[indexPath.row]
case 1:
title = tableDataWork[indexPath.row]
case 2:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row - tableDataWork.count]
}
default:
if indexPath.row < tableDataWork.count {
title = tableDataWork[indexPath.row]
} else {
title = tableDataHome[indexPath.row + tableDataWork.count]
}
}
cell?.textLabel?.text = title
if cell != nil {
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If
switch dataFilter {
case 0: return tableDataHome.count
case 1: return tableDataWork.count
default: return tableDataHome.count + tableDataWork.count
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1;
}
You can find the entire project here: https://github.com/ryantxr/segmented-control-app
It depends on your tableview.
If you use NSFetchedResultsController then you need to modify your fetch request. If you use an array directly, just use the filter function in Swift, passing in the condition, e.g. filteredArray = array.filter{$0.isAudioFile} Then, after setting your datasource array to the filtered one, call reloadData on your tableview.
You will need to keep a reference to the full array, and use the filtered one as your datasource in cellForRow...

UITableViewCell asynchronously loading images issue - Swift

In my app, I built my own asynchronous image loading class. I pass in a object, then it checks if the cache (NSCache) has the image, if not it will then check the file system if the image is saved already. If the image is not saved already, it will then download the image in the background (NSOperations help).
This works great so far, but I have ran into a few small issues with the table view loading the images.
First off, this is the function I use to set up the table view cell fromtableView(tableView:, willDisplayCell:, forRowAtIndexPath:)
func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {
// Configure cell
if let show = dataSource.showFromIndexPath(indexPath) {
ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
self.indexPathsForFadedInImages.append(indexPath)
if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
func fadeInImage() {
// Fade in image
fetchCell.backgroundImageView!.alpha = 0.0
fetchCell.backgroundImage = image
UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
fetchCell.backgroundImageView!.alpha = 1.0
})
}
if #available(iOS 9, *) {
if NSProcessInfo.processInfo().lowPowerModeEnabled {
fetchCell.backgroundImage = image
}
else {
fadeInImage()
}
}
else {
fadeInImage()
}
}
else {
// Issues are here
}
}
else {
// Set image
cell.backgroundImage = image
}
})
...
}
Where "// Issues are here" comment is, that is where I run into multiple issues.
So far, I have not figured out another way to validate that the image belongs to the cell for sure where "// Issues are here" is. If I add
cell.backgroundImage = image
there, then it fixes the issue where sometimes the image will not display on the table view cell. So far the only cause I have found for this is that the image is being returned faster than I can return the table view cell so that is why the table view says there is not a cell at that index path.
But if I add that code there, then I run into another issue! Cells will display the wrong images and then it lags down the app and the image will constantly switch, or even just stay on the wrong image.
I have checked that it runs on the main thread, image downloading and caching is all fine. It just has to do that the table is saying there is no cell at that index path, and I have tried getting an indexPath for the cell which returns also nil.
A semi-solution to this problem is called tableView.reloadData() in viewWillAppear/viewDidAppear. This will fix the issue, but then I lose the animation for table view cells on screen.
EDIT:
If I pass the image view into getImageForShow() and set it directly it will fix this issue, but that is less ideal design of code. The image view obviously exists, the cell exists, but for some reason it doesn't want to work every time.
Table views reuse cells to save memory, which can cause problems with any async routines that need to be performed to display the cell's data (like loading an image). If the cell is supposed to be displaying different data when the async operation completes, the app can suddenly go into an inconsistent display state.
To get around this, I recommend adding a generation property to your cells, and checking that property when the async operation completes:
protocol MyImageManager {
static var sharedManager: MyImageManager { get }
func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}
struct MyCellData {
let url: String
}
class MyTableViewCell: UITableViewCell {
// The generation will tell us which iteration of the cell we're working with
var generation: Int = 0
override func prepareForReuse() {
super.prepareForReuse()
// Increment the generation when the cell is recycled
self.generation++
self.data = nil
}
var data: MyCellData? {
didSet {
// Reset the display state
self.imageView?.image = nil
self.imageView?.alpha = 0
if let data = self.data {
// Remember what generation the cell is on
var generation = self.generation
// In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
weak var wcell = self
// Retrieve the image from the server (or from the local cache)
MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
if let error = error {
println("There was a problem fetching the image")
} else if let cell = wcell, image = image where cell.generation == generation {
// Make sure that UI updates happen on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Only update the cell if the generation value matches what it was prior to fetching the image
cell.imageView?.image = image
cell.imageView?.alpha = 0
UIView.animateWithDuration(0.25, animations: { () -> Void in
cell.imageView?.alpha = 1
})
})
}
})
}
}
}
}
class MyTableViewController: UITableViewController {
var rows: [MyCellData] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
cell.data = self.rows[indexPath.row]
return cell
}
}
A couple other notes:
Don't forget to do your display updates on the main thread. Updating on a network activity thread can cause the display to change at a seemingly random time (or never)
Be sure to weakly reference the cell (or any other UI elements) when you're performing an async operation in case the UI should be destroyed before the async op completes.