Gold Challenge for Chapter 9

I found the gold challenge interesting. My approach was to set up two sections ala the bronze challenge and label “Favorites” using a header. I used the footer to show “No items!” ala the silver challenge. To toggle “.isFavorite” i used the following in itemStore:

func favorite(index: IndexPath) -> Item {

    let item = allItems[index.section][index.row]
    item.isFavorite = true
    allItems[0].append(item)
   
    return item
}

In itemsView controller I added a trailingSwipeActionsConfigurationForRowAtIndexPath function with a UIContextualAction to preserve the delete function and add the isFavorite change while shifting favorites to a separate section in the table view:

override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration {

        let delete = UIContextualAction(style: .normal, title: "Delete") {(contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
            
            self.itemStore.allItems[indexPath.section].remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            completionHandler(true)
            }
            delete.title = "Delete"
            delete.backgroundColor = .red
            
        let favorite = UIContextualAction(style: .normal,
     title: "Favorite") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in
        
        let favoriteItem = self.itemStore.favorite(index: indexPath)
        
       if let index = self.itemStore.allItems[0].firstIndex(of: favoriteItem) {
    
           let path = IndexPath(row: index, section: 0)
        tableView.insertRows(at: [path], with: .automatic)
        
           self.itemStore.allItems[indexPath.section].remove(at: indexPath.row)
           tableView.deleteRows(at: [indexPath], with: .automatic)
       
        tableView .reloadData()
            }
        completionHandler(true)
        
            }
        favorite.title = "Favorite"
        favorite.backgroundColor = .blue

        return UISwipeActionsConfiguration(actions: [delete,favorite])
}

The isFavorite wasn’t really necessary to separate out the favorites by my method, but will be useful if I want to prevent moving the favorites back to their original section. I don’t have a clue if this is a great way to do this, but I did learn a lot of new code.

I don’t know if it was necessary to preserve the delete function the way you did.
And if I actually understood your code the favourite is set if you swipe to the left while you can also delete by swiping to the left because everything is under trailingSwipeActionsConfigurationForRowAt.

I think the best approach would be to separate them, since you are told to set a favourite when swiping right on them. What I did inside leadingSwipeActionsConf… was check in which section I am, since I have 2 arrays, one for items under 50 USD which are in section 0 and another for items over 50 USD in section 1, but this is all related to the bronze challenge. So find I the right item the user swiped right on through the subscript IndexPath on the appropriate array and this item has the property isFavorite which I set to true and then I reload this specific row, again with indexPath.

In cellForRowAt I retrieve the item again through the IndexPath on the right array depending on the section and then I check if it is a favourite. In case it is I show a star before the row’s title.

It seems as if you are inserting a row every time the user swipes, so if the item is a favourite it goes to a favourite section, right?
Which means there is no “over 50 USD” section anymore?

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        var item: Item!
        var items: [Item]!
        
        if indexPath.section == 0 {
            if showingFavorites {
                items = itemStore.allItemsUnderBoundary.filter({$0.isFavorite})
                item = items[indexPath.row]
            } else {
                item = itemStore.allItemsUnderBoundary[indexPath.row]
            }
        } else if indexPath.section == 1 {
            if showingFavorites {
                items = itemStore.allItemsOverBoundary.filter({$0.isFavorite})
                item = items[indexPath.row]
            } else {
                item = itemStore.allItemsOverBoundary[indexPath.row]
            }
        }
        
        if item.isFavorite {
            cell.textLabel?.text = "⭐️\t\t\(item.name)"
        } else {
            cell.textLabel?.text = "\(item.name)"
        }
    
        cell.detailTextLabel?.text = "$\(item.valueInDollars)"

        return cell
    }
    
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        print(editingStyle.rawValue)
        if editingStyle == .delete && !containsNoItem && !showingFavorites {
            if indexPath.section == 0 {
                let item = itemStore.allItemsUnderBoundary[indexPath.row]
                itemStore.removeItem(item)
            } else if indexPath.section == 1 {
                let item = itemStore.allItemsOverBoundary[indexPath.row]
                itemStore.removeItem(item)
            }
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
        if tableView.numberOfRows(inSection: 0) + tableView.numberOfRows(inSection: 1) == 0 {
            itemStore.allItemsUnderBoundary.append(no_ItemItem)
            tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
            containsNoItem = true
        }
    }

    
    override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        if containsNoItem {
            return nil
        }
        return UISwipeActionsConfiguration(actions: [UIContextualAction(style: .normal, title: "Favorite") {
            [weak self] _,_,_ in
            let item: Item!
            if indexPath.section == 0 {
                item = self?.itemStore.allItemsUnderBoundary[indexPath.row]
            } else {
                item = self?.itemStore.allItemsOverBoundary[indexPath.row]
            }
            item?.isFavorite = true
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }])
    }

    
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        if sourceIndexPath.section == 0 {
            itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row, isOverBoundary: false)
        } else if sourceIndexPath.section == 1 {
            itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row, isOverBoundary: true)
        }
    }
1 Like

You have a very good point. I changed my code to a leadingSwipeActionsConfiguration
ForRowAtIndexPath, removed the delete contextual action and it works fine. The right swipe makes it a favorite and the delete still works for a left swipe. The only thing I would add is preventing a row from being moved out of its section. There is also no way of removing the favorite tag in my code.

Out of the top of my head I would say you could achieve removing the favourite tag by checking if it is already a favourite when the user swipes right and clicks on “Favourite”.
Something like:
if item.isFavourite {
item.isFavourite = false
}
Or even simpler item.isFavourite = !item.isFavourite, assuming you initialized it with false.
I have not managed to prevent a row from being moved out of its section. If you manage to do it, I would love to know about it.

You’ll be interested in this function from the UITableViewDataSource protocol:

override func tableView(_ tableView: UITableView,
                        targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath,
                        toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath

This starts getting called when the user starts dragging an item around to move it. Every time the item is dragged over a new row in the table view it gets called again. If you want to allow the proposed move, you return the value in proposedDestinationIndexPath. If you want to not allow the item to move at all, you return the value in sourceIndexPath. And if you want to have the item move somewhere else instead, you can copy one of the index path arguments & modify it before returning it.

1 Like

Thanks. I’ll check it out and see what I can do with it.

Did anyone find a way to implement button-press filtering of favorite items?

You use a filter on the array:

func getItems() -> [Item]
{
    return itemStore.allItems.filter { (displayFavoritesOnly == false) || $0.favorite }
}

If the filter button is selected, the filter returns only favorited items; if the button is not selected it returns a copy of itemStore. Everywhere in itemViewController where you access itemStore directly, you replace that with a call to this filter function:

let item = getItems()[indexPath.row]

(The only exception would be when you’re adding a new item; you need to add the item to the actual itemStore, not to the filter).

Obviously you’ll need to tailor this depending on how you handled sections. But this should get you started.

1 Like