100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Building a Simple iOS App in Objective-C

A hands-on walkthrough of scaffolding, wiring, and running a basic iOS app entirely in Objective-C, from project setup to a working table view.

PracticeBeginner10 min readJul 10, 2026
Analogies

From Empty Project to Running App

Building even a simple iOS app in Objective-C touches the same core pieces every iOS app needs: an app delegate that handles lifecycle events, a storyboard or programmatic view hierarchy, at least one view controller, and a way to display dynamic data such as a UITableView. Walking through this end to end, starting from a new Xcode project with the Objective-C language option selected, is the fastest way to internalize how the pieces fit together before layering on networking or persistence.

🏏

Cricket analogy: Wiring up an app delegate before anything else is like a team appointing a captain first, before naming the batting order, someone needs to own the overall flow of the innings before individual roles make sense.

The App Delegate and Root View Controller

AppDelegate.m implements UIApplicationDelegate methods like application:didFinishLaunchingWithOptions:, where you typically create a UIWindow, set its rootViewController, and call makeKeyAndVisible. For a programmatic, storyboard-free app you instantiate your first view controller directly here, giving you full control over the initial navigation stack, which is especially useful when the launch flow needs conditional logic, such as showing an onboarding screen only on first launch.

🏏

Cricket analogy: Setting the rootViewController in the app delegate is like the toss deciding which team bats first, it determines the starting point every subsequent decision in the match builds on.

objectivec
// AppDelegate.m
#import "AppDelegate.h"
#import "TaskListViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    TaskListViewController *taskListVC = [[TaskListViewController alloc] init];
    UINavigationController *navController =
        [[UINavigationController alloc] initWithRootViewController:taskListVC];

    self.window.rootViewController = navController;
    [self.window makeKeyAndVisible];

    return YES;
}

@end

Displaying Data with UITableView

A view controller becomes a working table view by conforming to UITableViewDataSource and UITableViewDelegate, implementing tableView:numberOfRowsInSection: and tableView:cellForRowAtIndexPath:, and registering a reusable cell class with registerClass:forCellReuseIdentifier:. Cell reuse via dequeueReusableCellWithIdentifier:forIndexPath: is essential for performance: instead of allocating a new cell for every row, the table view recycles cells that have scrolled off screen, which keeps memory and CPU usage flat even for lists with thousands of rows.

🏏

Cricket analogy: Cell reuse is like a team reusing the same practice nets for every batter in the lineup instead of building a brand-new net for each individual player, far more efficient with the same resources.

Forgetting to dequeue cells properly, or configuring cell state without resetting it for reused cells, is a common source of bugs where table rows appear to 'remember' stale content from a previously scrolled-away row. Always fully configure every visible property of a reused cell in tableView:cellForRowAtIndexPath:, don't assume a fresh default state.

objectivec
// TaskListViewController.m
@interface TaskListViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, copy) NSArray<NSString *> *tasks;
@end

@implementation TaskListViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tasks = @[@"Buy groceries", @"Write report", @"Walk the dog"];

    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds
                                                   style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerClass:[UITableViewCell class]
            forCellReuseIdentifier:@"TaskCell"];
    [self.view addSubview:self.tableView];
}

- (NSInteger)tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section {
    return self.tasks.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
          cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TaskCell"
                                                              forIndexPath:indexPath];
    cell.textLabel.text = self.tasks[indexPath.row];
    return cell;
}

@end

Running and Debugging on the Simulator

With the app delegate and view controller wired up, selecting a simulator target and pressing Run compiles the project and launches it, giving you a live table view you can scroll and tap. From here, breakpoints set directly in Objective-C methods, combined with the LLDB console's po command for printing object descriptions, are the fastest way to inspect state, such as confirming the tasks array actually contains the expected strings before the table view renders them.

🏏

Cricket analogy: Setting a breakpoint to inspect the tasks array is like a coach pausing match footage at a specific ball to check exactly what field placement was in effect before the shot was played.

Once your table view works with a static array, the natural next step is wiring in NSURLSession to fetch remote JSON and reloading the table via [self.tableView reloadData] on the main thread after parsing, the same pattern scales from a hardcoded array to a fully dynamic, network-backed list.

  • Every iOS app needs an app delegate to handle lifecycle events and set up the initial UIWindow and root view controller.
  • A programmatic (storyboard-free) setup gives full control over the launch flow, useful for conditional onboarding logic.
  • UITableViewDataSource and UITableViewDelegate are the two protocols required to power a working table view.
  • Cell reuse via dequeueReusableCellWithIdentifier:forIndexPath: keeps memory and CPU flat even for very long lists.
  • Always fully configure every visible property of a reused cell to avoid stale content bugs.
  • Breakpoints plus the LLDB po command are the fastest way to inspect object state while debugging.
  • A static-array-backed table view is the natural stepping stone to a network-backed, dynamic list.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ObjectiveCStudyNotes#BuildingASimpleIOSAppInObjectiveC#Building#Simple#IOS#App#StudyNotes#SkillVeris#ExamPrep