Skip to main content
April 1, 2026Noble Desktop Publishing Team/10 min read

Lists: Improving the User Experience Programmatically

Professional iOS User Interface Enhancement Tutorial

Key UI Improvements Covered

Spacing Optimization

Learn to dynamically calculate spacing using UIEdgeInsets for better visual hierarchy between interface elements.

Cell Background Customization

Implement custom background images and colors for table view cell selection states across multiple screens.

Typography Enhancement

Apply conditional font weight changes to completed list items using ternary operators for better user feedback.

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

ex prev lists ux improvement

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.

Prerequisites Required

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

  1. Launch Xcode if it isn't already open.

  2. If you completed the previous exercise, Lists.xcodeproj should still be open. If you closed it, re-open it now.

  3. 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

1

Launch Development Environment

Open Xcode and ensure Lists.xcodeproj is loaded. If closed, navigate to File > Open and locate your project file.

2

Verify Prerequisites

Confirm completion of exercises 5A-6C or open the provided starter project from the Class Files directory.

3

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.

  1. If the Simulator isn't still running, go to Xcode and Run run icon it now.

  2. Make sure you are on the first screen with the list titles.

  3. 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:

    lists top inset comparison

  4. In Xcode, click on the ListsVC.swift tab (or open a new tab and navigate there now if you closed it).

  5. 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.

  6. 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————————————
  7. 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)
    }
  8. 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.
  9. Run run icon the Simulator to verify the improved spacing. Notice how the additional breathing room enhances readability and creates better visual flow.

  10. 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.

ViewDidLoad vs ViewWillAppear

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)
Dynamic spacing calculation that works across all iPhone sizes, found through trial and error testing.

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.

  1. Open the cell customization file by pressing Cmd–T, then clicking ListTitleTVCell.swift in the Project navigator.

  2. 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.

  3. Implement the background image swap based on selection state:

    if selected {
       backgroundImageView.image = UIImage(named: "CellSelectedBackground")
    } else {
       backgroundImageView.image = UIImage(named: "CellDeselectedBackground")
    }
  4. Now we'll eliminate the underlying gray highlight by matching it to our existing color scheme. Switch to the Main.storyboard tab.

  5. 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.

  6. Access the color specifications in the Attributes inspector attributes inspector icon.

  7. Click the Background color bar in the Attributes inspector.

  8. 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.

  9. Close the color picker and return to ListTitleTVCell.swift.

  10. 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)
    }
  11. Run run icon the Simulator to test the selection behavior.

  12. 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.

  13. Navigate to the second screen displaying list items.

  14. 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

FeatureDefault StateSelected State
Background ImageCellDeselectedBackgroundCellSelectedBackground
Background ColorDefault GrayCustom RGB (250,247,235)
User ExperienceGeneric appearanceBrand-consistent styling
Recommended: Custom backgrounds provide better visual consistency with your app's design system.
Color Value Conversion

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.

  1. Open ListItemTVCell.swift to customize the list item cell behavior.

  2. 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.

  3. 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.

  4. 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)
  5. Switch to ListItemsVC.swift to implement persistent state management.

  6. 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
    }
  7. Run run icon the Simulator to test both improvements.

  8. Navigate to any list and select an item by clicking anywhere except the check button. The refined yellow highlighting now maintains brand consistency.

  9. 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 Cell Recycling

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)
Ternary conditional operator that dynamically sets font weight based on completion status while maintaining the 17px San Francisco system font.

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.

  1. Return to Xcode and ensure proper file organization by selecting the Lists folder in the Project navigator project navigator icon.

  2. Create a new file using File > New > File or Cmd–N.

  3. Under iOS and Source, double–click the Cocoa Touch Class template.

  4. Configure the Navigation Controller subclass:

    Subclass of: UINavigationController (Set this first to ensure proper inheritance!)
    Class: NavigationController
  5. Click Next, then Create to generate the file.

  6. Clean up the generated template by retaining only essential code:

    import UIKit
    
    class NavigationController: UINavigationController {
    
       override func viewDidLoad() {
          super.viewDidLoad()
       }
    
    }
  7. 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.

  8. 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.

  9. Run run icon 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.

  10. Return to Xcode and open the Main.storyboard tab.

  11. Switch to the Identity inspector identity inspector icon in the right panel.

  12. Navigate to the Navigation Controller—the leftmost controller in our three-controller setup, marked as the initial controller.

  13. Click in the gray area of the Navigation Controller to select it.

  14. In the Identity inspector's Class field, type NavigationController. Accept Xcode's autocomplete suggestion and press Return.

  15. Run run icon 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

1

Create Navigation Controller Subclass

Use File > New > File, select Cocoa Touch Class template, and create a UINavigationController subclass named NavigationController.

2

Add Background Image

Set navigationBar.setBackgroundImage with a 9x9 pixel tiled image and enable translucency for proper visual effect.

3

Connect in Storyboard

In Identity Inspector, set the Navigation Controller's class to your custom NavigationController to apply the styling.

Image Tiling Technique

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.

Key Takeaways

1Use viewWillAppear instead of viewDidLoad for UI adjustments that need to respond to dynamic content changes, such as spacing calculations that depend on the number of list items.
2Implement custom cell selection states using the setSelected method in table view cells, combining custom background images with precise RGB color matching for consistent branding.
3Handle table view cell recycling by setting state-dependent properties in both the cell's action methods and the parent table view's cellForRowAt method to ensure visual consistency.
4Apply conditional styling using ternary operators to provide immediate visual feedback, such as bold text for completed items while maintaining system font consistency.
5Create custom navigation controller subclasses when you need to apply styling that cannot be achieved through Storyboard configuration, such as custom background images with translucency effects.
6Use dynamic calculations like frame.height/38 for spacing to ensure your interface looks proportional across different device sizes rather than using fixed pixel values.
7Remember to connect custom classes in the Identity Inspector after creating them - the code won't take effect until the Storyboard knows which file contains the relevant implementation.
8Leverage small tiled images for navigation bar backgrounds to achieve custom styling while maintaining memory efficiency and seamless scaling across screen sizes.

RELATED ARTICLES