Topics Covered in This iOS Development Tutorial:
Master advanced UI refinements including spacing optimization between the Add New List area and first list title, implementing dynamic background images for selected table view cells, applying bold formatting to completed list items, and customizing navigation controller headers with enhanced visual hierarchy.
Exercise Preview

Exercise Overview
With persistent storage now implemented, your app delivers genuine user value. However, polished applications distinguish themselves through thoughtful UI refinements that enhance usability and visual appeal. In this exercise, we'll implement professional-grade improvements using programmatic techniques that are standard in modern iOS development.
These enhancements address common UX pain points: cramped spacing that creates visual tension, inconsistent selection states that confuse users, insufficient visual feedback for completed actions, and weak navigation hierarchy. You'll learn to optimize spacing dynamically, implement custom cell selection behaviors, provide clear visual state indicators, and establish stronger visual hierarchy through navigation customization.
This exercise builds on previous work. Complete exercises 5A-6C first, or use the provided Lists Ready for UX Improvement starter project from Desktop > Class Files > yourname-iOS App Dev 2 Class.
Getting Started
Launch Xcode if it isn't already open.
If you completed the previous exercise, Lists.xcodeproj should still be open. If you closed it, re-open it now.
We recommend completing the previous exercises (5A–6C) before proceeding, as this builds directly on established persistence architecture. If you did not complete the previous exercises, follow these steps:
- Go to File > Open.
- Navigate to Desktop > Class Files > yourname-iOS App Dev 2 Class > Lists Ready for UX Improvement and double–click on Lists.xcodeproj.
Project Setup Process
Launch Development Environment
Open Xcode and ensure Lists.xcodeproj is loaded. If closed, navigate to File > Open and locate your project file.
Verify Prerequisites
Confirm completion of exercises 5A-6C or open the provided starter project from the Class Files directory.
Test Current State
Run the Simulator to observe the current UI before implementing enhancements.
Increasing the Spacing Between the Add New List Area & the First List Title
Professional iOS apps leverage dynamic spacing calculations to ensure consistent visual hierarchy across all device sizes. This approach scales beautifully and eliminates the cramped feeling that plagues many amateur applications.
If the Simulator isn't still running, go to Xcode and Run
it now.Make sure you are on the first screen with the list titles.
Examine the Add new list area below the navigation bar (containing the text field and Plus (+) button), then observe the first list title below it.
The current spacing creates visual tension and reduces readability—a common issue in rushed development. Professional apps provide generous breathing room that guides the user's eye naturally. Compare the cramped current state with our improved version:

In Xcode, click on the ListsVC.swift tab (or open a new tab and navigate there now if you closed it).
Locate the viewDidLoad method near the top of the code.
This method executes once when the view initially loads. While our existing loadLists() method call only needs single execution, our spacing adjustment must accommodate dynamic content changes. Since users can add multiple lists and our alphabetical sorting may change the topmost item, we need a method that responds to view state changes.
We'll override another UIViewController lifecycle method that handles multiple invocations gracefully.
Between viewDidLoad and the MARK comment, add the viewWillAppear method that executes before each visual presentation. (Accept Xcode's autocomplete suggestion to ensure proper method signature.)
override func viewDidLoad() { super.viewDidLoad() loadLists() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } // MARK:—TableView DataSource methods————————————Implement dynamic spacing by adding proportional inset calculation:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) listsTableView.contentInset = UIEdgeInsets(top: listsTableView.frame.height/38, left: 0, bottom: 0, right: 0) }Understanding the implementation:
- Table views default to zero inset, creating edge-to-edge content that often feels cramped in complex interfaces.
- UIEdgeInsets define spacing in points. We're using zero for left, bottom, and right edges while adding calculated top spacing.
- The 1/38 ratio was determined through design iteration and provides optimal visual balance across all iPhone sizes—from SE to Pro Max.
- This proportional approach ensures consistent visual hierarchy regardless of screen dimensions, a hallmark of professional iOS development.
Run
the Simulator to verify the improved spacing. Notice how the additional breathing room enhances readability and creates better visual flow.Test the selection interaction by clicking on a stored list name, then use the
<Lists button to return to the first screen.The default light gray selection background disrupts our carefully crafted color scheme. Professional apps maintain visual consistency by customizing selection states to complement their design system. We'll replace the generic gray with contextual background imagery that reinforces our app's visual identity.
Use viewDidLoad for one-time setup like loadLists(), but use viewWillAppear for code that needs to run multiple times as the interface changes, such as spacing adjustments that depend on dynamic content.
listsTableView.contentInset = UIEdgeInsets(top: listsTableView.frame.height/38, left: 0, bottom: 0, right: 0)Changing the Background Images When List Table View Cells Are Selected
Custom selection states are a hallmark of polished iOS applications. By replacing Apple's generic gray highlighting with purpose-built imagery, we create a more cohesive user experience that feels intentional rather than default.
Open the cell customization file by pressing Cmd–T, then clicking ListTitleTVCell.swift in the Project navigator.
Locate the setSelected function that Apple automatically generated. This method fires whenever a cell's selection state changes, making it perfect for our background swap logic. Add the conditional structure:
override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) if selected { // Handle selected state } else { // Handle deselected state } }This function executes every time a cell in the parent Table View changes state, providing the perfect hook for our visual customizations.
Implement the background image swap based on selection state:
if selected { backgroundImageView.image = UIImage(named: "CellSelectedBackground") } else { backgroundImageView.image = UIImage(named: "CellDeselectedBackground") }Now we'll eliminate the underlying gray highlight by matching it to our existing color scheme. Switch to the Main.storyboard tab.
On the first View Controller (center controller in our navigation flow), click slightly adjacent to the gradient background behind the List Name label to select the ListTitleTVCell.
Access the color specifications in the Attributes inspector
.Click the Background color bar in the Attributes inspector.
Note the RGB values and Opacity in the color picker. You should see Red: 250, Green: 247, Blue: 235, with 100% Opacity. These values represent our carefully chosen brand colors.
Close the color picker and return to ListTitleTVCell.swift.
Apply the matching background color to eliminate the gray highlight:
if selected { backgroundImageView.image = UIImage(named: "CellSelectedBackground") contentView.backgroundColor = UIColor(displayP3Red: 250/255, green: 247/255, blue: 235/255, alpha: 1) }Run
the Simulator to test the selection behavior.Select a cell to see the seamless image transition that now integrates perfectly with your design system. Navigate back to observe how the selection state maintains visual consistency.
Navigate to the second screen displaying list items.
Identify the remaining UX improvements needed:
- Click a list item title to see the persistent gray highlighting that still needs customization.
- Check an unchecked item. While the button image updates, users in hurried or distracted states may miss this subtle change. We'll add bold text formatting to provide unmistakable visual feedback.
Cell State Background Comparison
| Feature | Default State | Selected State |
|---|---|---|
| Background Image | CellDeselectedBackground | CellSelectedBackground |
| Background Color | Default Gray | Custom RGB (250,247,235) |
| User Experience | Generic appearance | Brand-consistent styling |
When using RGB values from design tools, remember to divide by 255 for iOS UIColor. Example: Red 250 becomes 250/255 in displayP3Red parameter.
Displaying a Completed List Item's Text in Bold
Effective task management apps provide clear, immediate visual feedback for completed actions. Bold text formatting serves as an unmistakable completion indicator that works even when users are quickly scanning their lists.
Open ListItemTVCell.swift to customize the list item cell behavior.
Replace the generic gray selection highlight with our branded light yellow. Update the setSelected method:
override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) if selected { contentView.backgroundColor = UIColor(displayP3Red: 252/255, green: 238/255, blue: 183/255, alpha: 1) } }This specific yellow was chosen by our design team to complement the overall color palette while providing sufficient contrast for accessibility compliance.
In the checkButtonTapped method, add dynamic font weight adjustment. This ternary operator efficiently handles the bold/regular state transition:
checkButton.setImage(item.checked ? UIImage(named: "Checked") : UIImage(named: "Unchecked"), for:.normal) itemNameLabel.font = item.checked ? UIFont.systemFont(ofSize: 17, weight: .bold) : UIFont.systemFont(ofSize: 17, weight: .regular) saveLists()This elegant conditional statement checks the item's completion state and applies appropriate font weight. We maintain the 17pt San Francisco system font for consistency while varying only the weight property.
Due to UITableView's cell recycling architecture, visual states don't persist automatically. Copy the font configuration line for use in the parent controller:
itemNameLabel.font = item.checked ? UIFont.systemFont(ofSize: 17, weight: .bold) : UIFont.systemFont(ofSize: 17, weight: .regular)Switch to ListItemsVC.swift to implement persistent state management.
In the cellForRowAt method within the TableView DataSource section, add the font state logic:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {Code Omitted To Save Space
listItemTVCell.itemNameLabel.text = listItemTVCell.item.title listItemTVCell.itemNameLabel.font = listItemTVCell.item.checked ? UIFont.systemFont(ofSize: 17, weight: .bold) : UIFont.systemFont(ofSize: 17, weight: .regular) listItemTVCell.checkButton.setImage(listItemTVCell.item.checked ? UIImage(named: "Checked") : UIImage(named: "Unchecked"), for:.normal) return listItemTVCell }Run
the Simulator to test both improvements.Navigate to any list and select an item by clicking anywhere except the check button. The refined yellow highlighting now maintains brand consistency.
Toggle multiple items between checked and unchecked states. The bold formatting provides immediate, unmistakable feedback that enhances usability, especially for users managing long task lists.
Test thoroughly with multiple items to verify that font weight changes reliably—this consistency results from our proper state management implementation rather than relying solely on cell-level storage.
Table View Cells get recycled but their content doesn't retain state. Always set font properties in both the cell's checkButtonTapped method AND the parent Table View's cellForRowAt method to ensure consistency.
itemNameLabel.font = item.checked ? UIFont.systemFont(ofSize: 17, weight: UIFontWeightBold) : UIFont.systemFont(ofSize: 17, weight: UIFontWeightRegular)Darkening the Header in the Navigation Controller
Strong visual hierarchy guides users through your application effortlessly. With our refined screens now exhibiting professional polish, the pale navigation header appears weak by comparison. We'll create a custom Navigation Controller subclass to implement programmatic header styling—a technique used in sophisticated iOS applications.
Return to Xcode and ensure proper file organization by selecting the Lists folder in the Project navigator
.Create a new file using File > New > File or Cmd–N.
Under iOS and Source, double–click the Cocoa Touch Class template.
Configure the Navigation Controller subclass:
Subclass of: UINavigationController (Set this first to ensure proper inheritance!) Class: NavigationController Click Next, then Create to generate the file.
Clean up the generated template by retaining only essential code:
import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() } }Implement custom navigation bar styling using a tiled background image. This approach provides consistent coloring while maintaining system-standard behavior:
override func viewDidLoad() { super.viewDidLoad() navigationBar.setBackgroundImage(UIImage(named: "NavigationBarBackground"), for: UIBar.Metrics.default) }Our custom background is a strategically designed 9×9 pixel image that tiles seamlessly both horizontally and vertically. This technique ensures consistent coloring across all device sizes while minimizing asset file size—a optimization technique used in production applications.
Address the translucency behavior that iOS determines based on image opacity. Since our image is fully opaque, we need to explicitly enable the subtle transparency effect that users expect from modern iOS interfaces:
override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.setBackgroundImage(UIImage(named: "NavigationBarBackground"), for: UIBar.Metrics.default) }This override ensures we maintain the sophisticated semi-transparent effect that characterizes modern iOS design, even with our custom opaque background image.
Run
the Simulator.If the navigation bar appears unchanged, don't worry—this is a common oversight in iOS development. We've created the custom class but haven't yet connected it to our Storyboard's Navigation Controller. This connection step is essential but easy to miss.
Return to Xcode and open the Main.storyboard tab.
Switch to the Identity inspector
in the right panel.Navigate to the Navigation Controller—the leftmost controller in our three-controller setup, marked as the initial controller.
Click in the gray area of the Navigation Controller to select it.
In the Identity inspector's Class field, type NavigationController. Accept Xcode's autocomplete suggestion and press Return.
Run
the Simulator once more.Now that Xcode knows which file contains the navigation customization logic, your enhanced header styling will render beautifully. The darker, richer navigation bar now provides the strong visual foundation that complements your refined interface elements.
Custom Navigation Controller Implementation
Create Navigation Controller Subclass
Use File > New > File, select Cocoa Touch Class template, and create a UINavigationController subclass named NavigationController.
Add Background Image
Set navigationBar.setBackgroundImage with a 9x9 pixel tiled image and enable translucency for proper visual effect.
Connect in Storyboard
In Identity Inspector, set the Navigation Controller's class to your custom NavigationController to apply the styling.
The NavigationBarBackground is only 9x9 pixels but tiles seamlessly across the entire navigation bar. This technique saves memory while providing consistent styling across all screen sizes.