misiel.

engineer ∙ gamer ∙ creator

Published on

Tags pt. 1

Authors

This is now the second side project I have worked on since transitioning to iOS development and the learnings are definitely compounding! I'm creating screens easier, I understand the navigation between different actions, there's less resistance between me envisioning an idea and putting it into code and much more.

Tags is an app inspired by the likes of apps like Pocket and Instapaper. Funny enough when I worked at Medium, I worked on the team that shipped a feature called "Save To Medium" which was inspired by the same apps! Things have come full circle now a couple years later I'm learning a new domain and building a similar app.

Even though I have a couple of years of mobile dev experience through Android development and it's been helping me piece things together way easier in iOS than when I first started in Android, a pattern in my side projects is I've been keeping the UI pretty simple. At least, just enough that the UI communicates what I want to the "user", but it isn't pretty or polished enough to ship to the app store. So something like having an "arrow drop down" button that reflects the open/closed state of the TagView in the table view of HomeViewController, is a little UI component that shows some intent.

open/close states of table view drop down

But enough prefacing, let's jump into the code!


TableView Section/Rows Animation

One thing I like to do before writing code is drawing up a couple rough wireframes of what I wanted different screens and the navigation to those screens to look like. One animation I knew I wanted was for each Tag to be displayed on the home screen, but for the items saved under those tags to appear when a user pressed a drop down button next to those tags. Conceptually, I thought I would have to create a table view to hold the tags and then have each row open up a nested table view for the saved items.

Here's a video showing the animation I wanted to achieve (it's also a tutorial on how to do it!):

Click to watch tutorial video on how to achieve table view fade animation
click to watch

Here's the code snippet of my tableView in HomeViewController:

//---HEADER---
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let tag = tags[section]
        let view = TagView()
        view.configure(with: tag)
        return view
    }

    // each Tag is a tableview section
    func numberOfSections(in tableView: UITableView) -> Int {
        return tags.count
    }

    //---SECTIONS & ROWS---
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let tag = tags[section]
        if (tag.isOpen == false) {
            return 0
        }
        return tag.taggedItems.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: DropDownCell.identifier, for: indexPath) as? DropDownCell else {return UITableViewCell()}
        cell.title.text = tags[indexPath.section].taggedItems[indexPath.row].title
        return cell
    }

So to achieve the drop down effect, I initally thought I would have to nest a table view within a table view, but I could actually do this by a distinction in views between the table view's Sections & Rows. In viewForHeaderInSection, TagView is a view that holds a button which when clicked, tells the HomeViewController that this specific tag has gotten its isOpen state changed. The state of this boolean value is what determines whether or not the rows below it will be shown as seen in numberOfRowsInSection. To react to this button change and show the rows, I use Combine to talk between the TagView button action and HomeViewModel table view:

// TagView.swift
@objc func showDropDown() {
        let isOpen = tagModel.isOpen
        // set Tag's open state to opposite of what it was
        tagModel.isOpen = !isOpen

        dropDownButton.isSelected = tagModel.isOpen
        HomeViewModel.shared.changeTagIsOpenState(tag: tagModel)
    }
// HomeViewModel.swift
HomeViewModel.shared.$tagWithChangedState
            .dropFirst()
            .sink { [weak self] tag in
                DispatchQueue.main.async {
                    var indexPaths = [IndexPath]()
                    // grab the section from the Tag object we get from the observable
                    guard let tagSection = self?.tags.firstIndex(of: tag) else {return}
                    // With the section, we can now grab the Tag's TaggedItems and add them into an IndexPaths object
                    guard let taggedItems = self?.tags[tagSection].taggedItems else {return}
                    for row in taggedItems.indices {
                        let idxPath = IndexPath(row: row, section: tagSection)
                        indexPaths.append(idxPath)
                    }

                    // Depending on the state, show/hide the TaggedItems in the rows
                    if tag.isOpen {
                        self?.tableView.insertRows(at: indexPaths, with: .fade)
                    }
                    else {
                        self?.tableView.deleteRows(at: indexPaths, with: .fade)
                    }
                }
            }
            .store(in: &cancellables)

Combine Coming in Clutch

Combine is a big cornerstone of this app and boy oh boy is it suuuuper useful. Even with the above example of the isOpen state change observation, I see how important it is to be able to handle user interactions and react accordingly throughout the app. Being that this app depends a lot on interacting with "tags" like adding/deleting, the main display of the tags (Home screen's table view) has to be aware of these changes at all times to be able to properly display the updated record of the user's tags. For every tag interaction method: saveTag(), deleteTag(), saveTagWithItem(), I end up calling HomeViewModel.refreshTags() which triggers an observable being observed by HomeViewController:

HomeViewModel.shared.$refreshTags
            .dropFirst()
            .sink { [weak self] tag in
                DispatchQueue.main.async {
                    self?.tags = Array(HomeViewModel.shared.getTags())
                    if (self?.tags.count == 0) {
                        self?.displayEmptyState()
                    }
                    else {
                        self?.displayFullState()
                        self?.tableView.reloadData()
                    }
                }
            }
            .store(in: &cancellables)

As you can see, self.tags gets refreshed with the latest value after one of the other modification methods metioned above have been called. Then in the main thread, since it's a UI update, we display our appropriate home screen state depending on our amount of tags and reload our table view data to reflect our current state of tags.


Conclusion

Observing user actions through reactive framework, screen navigations, data persistence and more are all core aspects of Tags, but in the next post I'll talk more about the biggest component and the one that led me to creating this app to learn how it worked: App Extensions!

See you in part 2!


Thanks for reading!