Build and Tooling

This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Objective-C’s toolchain is Clang plus the platform’s frameworks. On Apple platforms that means Xcode, which wraps compilation, linking, analysis, profiling and debugging in one place; elsewhere it means Clang or GCC plus GNUstep for a Foundation implementation.

Xcode and xcodebuild

Xcode is the reference environment. Its command-line counterpart, xcodebuild, is what CI systems use:

# Build a scheme
xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Debug build

# A workspace (required when CocoaPods is in play)
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp build

# Run the tests
xcodebuild test -workspace MyApp.xcworkspace -scheme MyApp \
          -destination 'platform=iOS Simulator,name=iPhone 15'

# Static analysis
xcodebuild analyze -workspace MyApp.xcworkspace -scheme MyApp

# Clean, archive and export
xcodebuild clean
xcodebuild archive -scheme MyApp -archivePath ./build/MyApp.xcarchive
xcodebuild -exportArchive -archivePath ./build/MyApp.xcarchive \
           -exportOptionsPlist ExportOptions.plist -exportPath ./build

# Inspect available destinations and settings
xcodebuild -showdestinations -scheme MyApp
xcodebuild -showBuildSettings -scheme MyApp

Two related tools: xcrun runs any tool from the active toolchain (xcrun clang …, xcrun simctl …), and xcode-select --install installs the Command Line Tools without the full Xcode application.

For direct compilation of a small program:

clang -fobjc-arc -fmodules -Wall -Wextra \
      -framework Foundation \
      main.m Person.m -o myapp

Compiler Flags

Flag Effect

-fobjc-arc

Enable ARC. Per-file; -fno-objc-arc disables it for one translation unit.

-fmodules

Enable Clang modules, so @import works and framework headers are precompiled.

-fobjc-weak

Enable __weak references (implied by -fobjc-arc).

-framework Foundation

Link a framework. -framework UIKit, -framework AppKit, and so on.

-ObjC

Load every Objective-C class and category from static libraries. Required when a static library contains categories, or their methods vanish at run time.

-all_load / -force_load <lib>

Heavier variants of the same idea.

-Wall -Wextra

Turn on the useful warnings. Non-negotiable.

-Werror

Treat warnings as errors. Excellent in CI; adopt incrementally on an existing codebase.

-Wno-<name>

Silence one specific warning, e.g. -Wno-unused-parameter.

-O0 / -O2 / -Os

Optimisation: none (debug), speed, size.

-g

Emit debug symbols.

-fobjc-arc-exceptions

Emit ARC cleanup on exception paths. Off by default; costs performance.

-x objective-c++

Compile as Objective-C++ (implied by the .mm extension).

-fsanitize=address / thread / undefined

Enable a sanitizer.

Warnings worth enabling beyond -Wall -Wextra: -Wundeclared-selector (catches @selector typos), -Wdeprecated-declarations, -Wdocumentation, and -Wobjc-missing-property-synthesis.

Building Elsewhere

Linux with GNUstep

# Install (Debian/Ubuntu)
sudo apt install gnustep gnustep-devel gobjc clang

# Compile and link, letting gnustep-config supply the flags
clang `gnustep-config --objc-flags` hello.m -o hello `gnustep-config --base-libs`

# With GCC instead
gcc `gnustep-config --objc-flags` hello.m -o hello `gnustep-config --base-libs`

gnustep-config --objc-flags emits include paths and runtime-selection flags; --base-libs emits the linker flags for libgnustep-base, GNUstep’s Foundation implementation.

What differs from Apple’s toolchain, in practice:

  • ARC works with Clang and the modern libobjc2 runtime, but not with GCC’s GNU runtime — legacy GNUstep code is usually MRR.

  • Clang modules (@import) are generally unavailable; use #import.

  • Only Foundation-level APIs exist. UIKit is absent entirely; AppKit has a partial GNUstep counterpart.

  • Blocks require -fblocks and the BlocksRuntime library.

GNUstep is the practical route for command-line tools and portable model code, not for UI.

Windows

Two options: GNUstep under MSYS2/MinGW (the same story as Linux), or WSL with the Linux instructions above. Native Windows Objective-C development is a niche activity — for cross-platform code, keep the Objective-C confined to a Foundation-only core and write the UI per platform.

The Clang Static Analyzer

The analyzer explores execution paths without running the program, and finds memory, nullability and logic errors that neither the compiler’s warnings nor your tests will catch.

xcodebuild analyze -workspace MyApp.xcworkspace -scheme MyApp
clang --analyze -fobjc-arc MyClass.m

In Xcode it is ⇧⌘B (Product ▸ Analyze), and Analyze During Build runs it on every build. What it finds:

  • Leaked and over-released objects, including at the Core Foundation boundary.

  • Mismatched bridging casts.

  • Dereferencing a value that can be nil on some path.

  • Values stored but never read, and logic that can never execute.

  • API misuse it has been taught about.

Run it routinely — it is the cheapest quality tool in the Objective-C toolbox, and it complains far less often than a linter would.

Sanitizers

Sanitizers instrument the binary and report errors at the moment they happen, with a stack trace. Enable them in the scheme’s Diagnostics tab, or on the command line.

Sanitizer Flag

Finds

Address

-fsanitize=address

Use-after-free, buffer overflow, use-after-return, double-free. ~2× slower.

Thread

-fsanitize=thread

Data races, including ones your tests do not reproduce. 5-15× slower.

Undefined Behavior

-fsanitize=undefined

Signed overflow, misaligned or null pointers, invalid casts.

Main Thread Checker

(Xcode scheme option)

UIKit/AppKit calls made off the main thread.

Malloc Scribble / Guard Edges

environment variables

Heap corruption.

Address and Thread Sanitizer cannot be enabled together. The practical routine is to run the test suite under ASan regularly and under TSan periodically — TSan in particular finds races that will otherwise surface as irreproducible field crashes. Leave the Main Thread Checker on always; it costs nothing noticeable.

Instruments

Instruments is the profiler, driven by templates:

Template Use

Leaks

Memory with no remaining references. Run it, exercise the app, look for the growth.

Allocations

Every allocation, with Mark Generation to isolate what a given operation leaves behind  — the best tool for finding retain cycles.

Zombies

Messages sent to deallocated objects. Turns a corrupted-memory crash into a precise report.

Time Profiler

Where CPU time goes, by stack frame.

Network / File Activity

I/O behaviour.

Energy Log

Battery impact of your QoS choices.

The retain-cycle routine with Allocations is worth memorising: mark a generation, perform the operation you suspect, return the app to its starting state, mark again — anything still alive in the newer generation is a candidate.

For a cycle you can already reproduce, the Memory Graph Debugger in Xcode (the graph icon in the debug bar) is faster: it draws the live object graph and annotates leaked objects with a purple !.

# Command-line profiling
xcrun xctrace record --template 'Time Profiler' --launch -- ./myapp

Debugging with lldb

(lldb) po myObject                 # print the object's -debugDescription
(lldb) p myInteger                 # print a scalar / expression
(lldb) p (int)[array count]        # cast when lldb cannot infer the return type
(lldb) expr self.name = @"Ada"     # evaluate an expression, with side effects

(lldb) bt                          # backtrace for this thread
(lldb) bt all                      # every thread
(lldb) frame variable              # all locals in the current frame
(lldb) up / down                   # move through frames

(lldb) b MyClass.m:42              # breakpoint by file and line
(lldb) b -[MyClass doThing:]       # breakpoint on a method
(lldb) breakpoint set -E objc      # break on every Objective-C exception THROWN

(lldb) c / n / s / finish          # continue / step over / step in / step out
(lldb) image lookup -a 0x10a2b3c4  # symbolicate an address

The exception breakpoint deserves its own mention. By default, an uncaught exception surfaces deep inside objc_exception_throw with a useless stack. Adding an "All Objective-C Exceptions" breakpoint (the + at the bottom of Xcode’s breakpoint navigator, or breakpoint set -E objc) stops execution at the throwing line instead. Set it once per project and leave it.

Two more habits: po uses debugDescription, so overriding that on your model classes pays for itself immediately (see Classes and Objects); and a symbolic breakpoint on -[UIView setFrame:] or similar is often quicker than hunting for a caller.

Dependency Managers

Tool Notes

Swift Package Manager

Apple’s own, integrated into Xcode, and it supports Objective-C targets. The default choice for new work.

CocoaPods

The long-established Objective-C dependency manager, with by far the largest catalogue of Objective-C libraries. Generates an .xcworkspace you must open instead of the .xcodeproj.

Carthage

Builds dependencies into frameworks and leaves integration to you. Less common now.

# Podfile
platform :ios, '15.0'
use_frameworks!

target 'MyApp' do
  pod 'AFNetworking', '~> 4.0'
  pod 'OCMock', '~> 3.9'
end
pod install          # install and generate MyApp.xcworkspace -- open THIS, not the .xcodeproj
pod update
pod outdated

A Swift package with an Objective-C target:

// Package.swift
targets: [
    .target(
        name: "MyObjCLibrary",
        path: "Sources/MyObjCLibrary",
        publicHeadersPath: "include"      // the directory clients import from
    )
]

The include directory convention matters: SwiftPM exposes exactly those headers, so the public/private split of Modules, Frameworks and Code Organization maps onto directory layout here.

Publishing a Library

Unlike Kotlin’s pointer to Java’s Maven Central section (an already-documented registry, so nothing more than a cross-reference is needed), CocoaPods has no canonical page of its own anywhere on this site — so it gets a full walkthrough here, while the SPM-registry angle below still just cross-references Swift’s page rather than repeating it.

CocoaPods is the long-established path for distributing an Objective-C library, and remains the option with the largest catalogue of existing Objective-C consumers. Publishing starts with a one-time CocoaPods Trunk account, registered by email:

pod trunk register your.email@example.com 'Your Name'

The account confirms via a link sent to that email, after which the machine holds a session token in ~/.netrc used by every subsequent pod trunk push. The library itself is described by a .podspec at its root — a minimal one:

# MyObjCLibrary.podspec
Pod::Spec.new do |s|
  s.name         = "MyObjCLibrary"
  s.version      = "1.0.0"
  s.source       = { :git => "https://github.com/example/MyObjCLibrary.git", :tag => s.version.to_s }
  s.source_files = "Sources/**/*.{h,m}"
  s.platform     = :ios, "15.0"
end

pod spec lint validates the podspec against a real build before anything is pushed — catching a missing file pattern or an unresolved dependency locally rather than after publishing:

pod spec lint MyObjCLibrary.podspec
pod trunk push MyObjCLibrary.podspec

An Objective-C target can also be distributed as a Swift package rather than (or alongside) a CocoaPod — see Swift Package Manager for the SPM-registry angle covered there, which applies to an Objective-C target the same way it does to a Swift one.

A tag-gated GitHub Actions release workflow, authenticated with a CocoaPods Trunk token stored as a repository secret:

name: build

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  pull_request:

jobs:
  build:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v4

      - name: Install CocoaPods
        run: gem install cocoapods

      - name: Lint podspec
        run: pod spec lint --allow-warnings

      - name: Publish to CocoaPods Trunk
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
        run: pod trunk push --allow-warnings

COCOAPODS_TRUNK_TOKEN is read directly from the environment by pod trunk push, per CocoaPods' own CI documentation — no ~/.netrc file needs to be checked in or generated in the workflow.

clang-format

Automated formatting, so style stops being a review topic:

# .clang-format
BasedOnStyle: LLVM
Language: ObjC
ColumnLimit: 120
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
IndentWidth: 4
AllowShortFunctionsOnASingleLine: false
clang-format -i MyClass.m                           # format one file in place
find . -name '*.m' -o -name '*.h' | xargs clang-format -i
clang-format --dry-run --Werror MyClass.m           # check only -- good for CI

Add the CI check before reformatting an existing codebase wholesale, so the diff stays reviewable.

For linting proper, oclint offers rule-based static analysis beyond what the Clang analyzer does, though the analyzer covers most of the value.

See Also