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

Lists: Programming the List Items View Controller

Master iOS List Management and Table View Programming

Core Concepts in This Tutorial

Implicitly Unwrapped Optionals

Learn when and why to use ! instead of ? for optional properties that will always have values when accessed.

Table View Data Management

Master the data source and delegate methods essential for iOS table view functionality.

Dynamic UI Updates

Implement responsive button image changes and navigation customization based on user interactions.

Topics Covered in This iOS Development Tutorial:

Master advanced iOS development concepts including implicitly unwrapping properties that cannot be defined during initialization, implementing comprehensive table view data source and delegate methods with UI responsive functionality, creating dynamic user interactions by changing check button images on tap, and customizing navigation item appearance through tint and title modifications.

Exercise Preview

ex prev lists programming list items vc

Exercise Overview

In this comprehensive exercise, you'll build and program the List Items View Controller—the destination screen users navigate to when they want to view and manage individual items within their lists. This controller serves dual roles as both the data source and delegate for its associated table view, requiring us to implement familiar patterns with strategic modifications.

You'll leverage code reusability by adapting methods from the previous exercise, making targeted adjustments to fit our new context. Additionally, we'll implement dynamic user interface behavior by programming a responsive check button system. When users tap an unchecked item, the interface will instantly update to display a checked state, and vice versa—creating an intuitive, immediate feedback loop that enhances the user experience.

This exercise demonstrates real-world iOS development practices where code reuse, proper delegation patterns, and responsive UI design converge to create professional-quality applications.

Tutorial Implementation Flow

1

Set Up View Controller

Configure the List Items View Controller with proper data source and delegate protocols

2

Copy and Adapt Methods

Transfer table view methods from the previous controller and modify them for list items

3

Implement Interactive Features

Add check button functionality and navigation customization for enhanced user experience

Getting Started

  1. Launch Xcode if it isn't already running on your system.

  2. If you completed the previous exercise successfully, your Lists.xcodeproj should remain open in your workspace. If you've closed it, navigate back and reopen the project file now.

  3. We strongly recommend completing the foundational exercises (5A–6A) before proceeding with this advanced implementation. If you haven't completed the prerequisite exercises, follow these steps to access the prepared starter project:

    • Navigate to File > Open from the menu bar.
    • Browse to Desktop > Class Files > yourname-iOS App Dev 2 Class > Lists Ready for List Items View Controller and double-click the Lists.xcodeproj file.
  4. Ensure your workspace includes these essential files in separate editor tabs: Main.storyboard, Data Model.swift, ListsVC.swift, and ListItemsVC.swift. If any are missing from your tab bar, open them now using the Cmd–T shortcut for new tabs.

Prerequisites Checklist

0/4

Understanding Implicitly Unwrapped Optionals: A Deep Dive

Before we begin adapting code from our first view controller, let's examine a crucial Swift concept that often puzzles developers: implicitly unwrapped optionals. Understanding this concept is essential for writing safe, effective iOS applications.

  1. Switch to the ListItemsVC.swift file by clicking its tab in the editor.

  2. Locate the list variable declaration in the middle section of your code:

    var list: List!

    Notice this property uses an exclamation mark (!) rather than the typical question mark (?) syntax for optionals. This creates an implicitly unwrapped optional—a powerful but potentially dangerous Swift feature that requires careful consideration.

  3. Let's explore how Swift handles optionals under the hood. Add this temporary variable to see the underlying Optional type structure:

    var list: List!
    var wrappedOptional: Optional<String>
    
    override func viewDidLoad() {

    This longhand syntax is functionally identical to var wrappedOptional: String? but reveals Swift's internal implementation.

  4. Hold CTRL–Cmd and click on Optional to examine Swift's source code for this fundamental type.

    You'll see an enumeration called Optional<Wrapped> with public access, making it available throughout the Swift ecosystem.

  5. Observe the two distinct cases that define optional behavior: none represents nil values, while some(Wrapped) securely stores actual values within a protective wrapper. This wrapper system prevents runtime crashes by ensuring safe value access patterns.

  6. Return to ListItemsVC.swift using the Go Back button go back button located above the code editor.

  7. Clean up your code by removing the temporary wrappedOptional declaration (use Cmd–Z to undo until the line disappears).

  8. Now let's understand the critical distinction between these optional types:

    • Standard optionals store values within protective wrappers that must be explicitly unwrapped using the ! operator when you're certain a value exists. This explicit unwrapping provides safety checkpoints in your code.
    • Implicitly unwrapped optionals automatically remove the protective wrapper, eliminating the need for explicit unwrapping syntax. However, this convenience comes with increased responsibility—your application will crash if you access a nil implicitly unwrapped optional.
  9. To understand why we're confident our list variable will always contain a value, switch to the ListsVC.swift tab and examine our navigation logic.

  10. Find the prepare method that handles view controller transitions:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
       let listItemsVC = segue.destination as! ListItemsVC
       listItemsVC.list = lists[(listsTableView.indexPathForSelectedRow?.row)!]
    }

    This method only executes when a user taps a table view row, guaranteeing that a valid list object exists. The user interface prevents navigation when no data is available, eliminating the possibility of nil assignment.

  11. The timing of value assignment explains why we need optional declaration at all. Consider this initialization sequence:

    let listItemsVC = segue.destination as! ListItemsVC

    This line instantiates the ListItemsVC class, but the list property remains uninitialized at this moment.

  12. The subsequent line assigns the actual value:

    listItemsVC.list = lists[(listsTableView.indexPathForSelectedRow?.row)!]

    This creates a brief window between class initialization and property assignment, necessitating optional declaration even though we guarantee the value will exist before any access attempts.

  13. Return to ListItemsVC.swift to see additional examples of implicitly unwrapped optionals.

  14. Examine these outlet declarations near the top of your file:

    @IBOutlet weak var newItemNameTextField: UITextField!
    @IBOutlet weak var itemsTableView: UITableView!
  15. These outlets use implicit unwrapping because Interface Builder guarantees their connection when the storyboard loads correctly. If your app crashes during initial testing, it often indicates a broken outlet connection—making implicitly unwrapped outlets useful debugging tools. The view controller instantiation occurs before view loading, creating the same initialization timing issue we discussed with the list property.

Implementing Table View Data Source and Delegate Methods

With our understanding of optionals solidified, let's implement the core functionality by adapting our existing table view methods for the new context.

  1. Add the required protocol conformance to enable table view functionality:

    class ListItemsVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
  2. Switch to ListsVC.swift to copy our proven table view implementation.

  3. Select and copy this complete code block (including all MARK comments and their associated methods):

    // MARK:—TableView DataSource methods————————————

    Two Methods Omitted To Save Space

    // MARK:—TableView Delegate methods————————————

    Method Omitted To Save Space

    // MARK:—UI responsive methods————————————
  4. Return to ListItemsVC.swift and paste the methods below your viewDidLoad implementation:

    override func viewDidLoad() {
          super.viewDidLoad()
       }
    
    
    // MARK:—TableView DataSource methods————————————

    Code & Additional Mark Comment Omitted To Save Space

    // MARK:—UI responsive methods————————————
    
       @IBAction func addItemButtonTapped(_ sender: Any) {
    
       }

Leveraging Xcode's Edit All in Scope for Efficient Code Modification

Now we'll adapt our copied methods using Xcode's powerful refactoring tools, demonstrating professional development workflows that save time and reduce errors.

  1. In the numberOfRowsInSection data source method, locate and select the lists variable:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return lists.count }

    This method determines the number of rows our table view will display—a fundamental building block of table view architecture.

  2. Right-click the selected word and choose Edit All in Scope from the context menu.

  3. Verify that all three instances of lists are highlighted across these methods:

    • cellForRowAt data source method (responsible for configuring cell content):
    listTitleTVCell.listTitleLabel.text = lists[indexPath.row].title
    • commit delegate method (handles cell deletion functionality):
    lists.remove(at: indexPath.row)
  4. Replace all highlighted instances with list.items, creating this updated code structure:

    • Data source numberOfRowsInSection:
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return list.items.count }
    • Data source cellForRowAt:
    listTitleTVCell.listTitleLabel.text = list.items[indexPath.row].title
    • Delegate commit:
    list.items.remove(at: indexPath.row)
  5. Let's examine the data model structure supporting these changes. Switch to Data Model.swift to understand the relationship hierarchy.

  6. Within the List class, find the items property we're now referencing:

    var items = [ListItem]() { didSet { items.sort() { $0.title < $1.title } } }

    This property maintains an array of ListItem instances with automatic alphabetical sorting via the didSet property observer—ensuring consistent, organized data presentation regardless of insertion order.

    This demonstrates a one-to-many relationship where each List can contain multiple ListItem objects, explaining why we navigate through list.items to access individual items.

  7. Continue refactoring by returning to ListItemsVC.swift.

  8. In the cellForRowAt method, select listTitleTVCell. Focus on the return statement for easier identification:

    return listTitleTVCell
  9. Use the keyboard shortcut Cmd–CTRL–E to select all instances of this variable name efficiently.

  10. Replace all instances with listItemTVCell:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let listItemTVCell = tableView.dequeueReusableCell(withIdentifier: "ListTitleTVCell", for: indexPath) as! ListTitleTVCell
       listItemTVCell.listTitleLabel.text = list.items[indexPath.row].title
       return listItemTVCell
    }
  11. Complete the cell configuration by updating the identifier and type references:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let listItemTVCell = tableView.dequeueReusableCell(withIdentifier: "ListItemTVCell", for: indexPath) as! ListItemTVCell
       listItemTVCell.itemNameLabel.text = list.items[indexPath.row].title
       return listItemTVCell
    }

    Note that Xcode treats string literals and type names as distinct contexts, preventing bulk replacement across different semantic categories—a safety feature that prevents unintended changes.

  12. Now let's implement the add functionality. Switch to ListsVC.swift to copy the button action logic.

  13. In the addListButtonTapped method, select and copy these three core lines:

    if newListNameTextField.text == "" { return }
    lists.append(List(listTitle: newListNameTextField.text!)!)
    listsTableView.reloadData()
  14. Return to ListItemsVC.swift and paste the code into the addItemButtonTapped method:

    @IBAction func addItemButtonTapped(_ sender: Any) {
       if newListNameTextField.text == "" { return }
       lists.append(List(listTitle: newListNameTextField.text!)!)
       listsTableView.reloadData()
    }

    The red errors indicate references to outlets that don't exist in this view controller context—exactly what we need to fix.

  15. Manually update the references to match our current view controller's outlets and data model:

    @IBAction func addItemButtonTapped(_ sender: Any) {
       if newItemNameTextField.text == "" { return }
       list.items.append(ListItem(itemTitle: newItemNameTextField.text!)!)
       itemsTableView.reloadData()
    }

    This implementation creates new ListItem instances and adds them to our data model before refreshing the table view display. The guard condition prevents empty entries, maintaining data quality throughout the user experience.

Creating Dynamic Check Button Interactions

Now we'll implement one of the most engaging aspects of our app: dynamic check button behavior that provides immediate visual feedback to users. This feature leverages the checked boolean property in our ListItem class to create an intuitive task completion system.

The ListItem model includes two essential attributes: the title property for user-entered names, and the checked boolean for completion status. New items default to false (unchecked), reflecting incomplete tasks. We'll use this state to dynamically update button imagery.

  1. Since our button interaction logic resides in the custom table view cell, we need to establish a connection to our data model. Create a new tab with Cmd–T and open ListItemTVCell.swift from the Project navigator.

  2. Add an implicitly unwrapped optional property to reference individual list items:

    @IBOutlet weak var checkButton: UIButton!
    @IBOutlet weak var itemNameLabel: UILabel!
    
    var item: ListItem!
    
    @IBAction func checkButtonTapped(_ sender: Any) {
  3. Return to ListItemsVC.swift to establish the connection between our table view cells and their corresponding data model objects.

  4. In the cellForRowAt method, add this crucial line to link each cell with its data:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let listItemTVCell = tableView.dequeueReusableCell(withIdentifier: "ListItemTVCell", for: indexPath) as! ListItemTVCell
       listItemTVCell.itemNameLabel.text = list.items[indexPath.row].title
       listItemTVCell.item = list.items[indexPath.row]
       return listItemTVCell
    }

    This assignment ensures each table view cell maintains a direct reference to its corresponding ListItem object, enabling seamless state management and user interactions.

Key Takeaways

1Implicitly unwrapped optionals use ! syntax and automatically unwrap values, but crash if nil - only use when values are guaranteed to exist
2The Optional type in Swift is an enumeration with 'none' for nil values and 'some(Wrapped)' for actual values stored in protective wrappers
3Table view controllers require UITableViewDataSource and UITableViewDelegate protocols to manage data display and user interactions
4Edit All in Scope (Cmd-Ctrl-E) enables simultaneous editing of multiple variable instances within the current code scope for efficient refactoring
5Properties that cannot be initialized during class instantiation must be declared as optionals, even if they will always have values later
6Outlet connections are always implicitly unwrapped optionals because they will have values once the view loads, helping identify Storyboard setup errors
7The didSet property observer automatically sorts array elements when new instances are added to maintain consistent ordering
8Code reusability is achieved by copying methods between similar view controllers and adapting variable references to match the new context

RELATED ARTICLES