- Published on
Tags pt. 1
- Authors
- Name
- Misiel Rodriguez
- @misiel
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.
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!):
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!