iOS Monoliths! Use Modular Architecture Instead

B
Bright Coding
Author
Share:
iOS Monoliths! Use Modular Architecture Instead
Advertisement

Stop Building iOS Monoliths! Use Modular Architecture Instead

Your AppDelegate is 2,000 lines. Your build times crawl past coffee breaks. One feature change triggers 47 unrelated test failures. Sound familiar? You're trapped in monolithic iOS hell—and it's costing your team velocity, sanity, and sleep.

But what if I told you there's a battle-tested escape route? A template so clean that OLX engineers publicly shared it as their architectural blueprint? Enter iOS Modular Architecture—the open-source template that's making senior iOS developers abandon massive view controllers forever.

This isn't theoretical fluff. The repository contains real CocoaPods-based module extraction, complete with dependency graphs, CI/CD pipelines, and step-by-step video guides. Whether you're drowning in a 500-file project or architecting your next app from scratch, this template exposes the exact modular patterns that let teams ship features in parallel without merge-conflict warfare.

Ready to see how scalable services actually work in production iOS apps? Let's dismantle the monolith—module by module.


What is iOS Modular Architecture?

iOS Modular Architecture is a production-ready template repository created by Oleh Kudinov (iOS Engineer at OLX) that demonstrates how to decompose massive iOS applications into independent, reusable, and testable modules using CocoaPods local development pods.

Originally published alongside a detailed Medium post from OLX's engineering blog, this repository exploded in popularity because it solves a pain point every scaling iOS team hits: the point where MVC becomes Massive View Controller, and MVVM isn't enough to save you.

The template uses a Movie app as its demonstration vehicle—easily replaceable with your own domain by swapping the "Movie" naming convention. But the real magic isn't the sample content; it's the structural scaffolding that shows exactly how to:

  • Extract networking layers into standalone service modules
  • Isolate feature modules (like search, authentication, or chat) with clean boundaries
  • Share resources and dependencies without circular import nightmares
  • Run CI/CD pipelines that verify every module builds independently before app integration

The repository includes visual dependency graphs showing how modules interconnect, YouTube walkthroughs for hands-on extraction, and production-grade configurations for both dynamic and static library compilation. It's trending because it bridges the gap between "I know I should modularize" and "here's exactly how to do it without breaking everything."


Key Features That Make This Template Insane

Let's dissect what makes this architecture template genuinely production-worthy—not just another GitHub toy project.

Local Development Pods as Module Containers

The template leverages CocoaPods' :path directive to treat modules as local development pods. This means each module lives in its own folder, has its own .podspec, and can be developed in isolation using dedicated Example apps—yet still integrates seamlessly into the main workspace.

Hierarchical Dependency Management

The included dependency graphs reveal a directed acyclic structure: Networking Service sits at the foundation, Feature Modules consume services, and the main App orchestrates everything. No circular dependencies. No hidden coupling. Just clean, inspectable boundaries.

Authentication Module as Scaling Proof

The repository demonstrates initial scaling by including an Authentication module that layers on top of the base architecture. This proves the template isn't just for trivial demos—it handles real cross-cutting concerns that production apps require.

Resource Bundle Isolation

Images, storyboards, Core Data models, and XIBs don't leak between modules. The template shows exact Bundle(for: Self.self) patterns and even covers the static library conversion with resource_bundles for optimal launch performance.

CI/CD-Ready Testing Infrastructure

With Fastlane and Travis CI configurations included, every module gets build verification on every commit. The scan action tests each module scheme independently before running full app tests—catching breakage at the source, not during integration.

Video-Guided Extraction Process

Two detailed YouTube videos walk through Networking Service extraction and Movies Search Feature Module extraction with timestamps for every step. This isn't documentation you skim; it's follow-along education.


Real-World Use Cases Where This Architecture Dominates

1. Enterprise Teams with 10+ Developers

When multiple engineers touch the same codebase daily, merge conflicts become existential threats. Modular Architecture lets Team A own Authentication, Team B own Search, and Team C own Networking—all shipping independently without stepping on each other's git blame.

2. White-Label App Platforms

Building a core platform that spawns branded variants? Extract shared services (analytics, networking, UI components) into reusable modules, then compose per-client feature modules without forking entire repositories.

3. Legacy Monolith Migration

That 5-year-old app with 800 Swift files? The template's step-by-step extraction process—complete with video timestamps—provides a safe incremental migration path. Extract one module per sprint, verify with CI, never ship broken builds.

4. SDK and Framework Development

If your company ships iOS SDKs to external developers, this architecture demonstrates proper module boundary design. Your networking layer becomes a consumable pod. Your UI components become standalone packages. Clean contracts, clean distribution.


Step-by-Step Installation & Setup Guide

Prerequisites

  • Xcode 11.2.1+ (though modern Xcode versions work with minor adjustments)
  • Swift 5.0+
  • CocoaPods 1.8.4+

Step 1: Clone and Initialize

# Clone the template repository
git clone https://github.com/kudoleh/iOS-Modular-Architecture.git
cd iOS-Modular-Architecture

# Initialize CocoaPods for the main App
pod init

Edit the generated Podfile to configure your workspace:

# Set minimum iOS version (modernize to 14.0 or 15.0)
platform :ios, '15.0'

# Define workspace name
workspace 'AppName.xcworkspace'

# Reference main project
project 'AppName.xcodeproj'

Install dependencies:

pod install

Step 2: Create Your First Module

# Create development pods directory
mkdir DevPods
cd DevPods

# Generate module scaffold (interactive prompts follow)
pod lib create ModuleName
# Select: iOS, Swift, Yes for Demo App

Step 3: Configure Module Standards

Open the generated Example project and enforce consistency:

  • Set Deployment Target to iOS 14.0/15.0
  • Set Swift Language Version to Swift 5
  • Delete Test Target (we'll reconfigure tests properly later)
  • Close the Example project (critical—workspace conflicts otherwise)

Step 4: Clean Module Skeleton

# Show hidden files and remove version control artifacts
Cmd + Shift + .  # In Finder
rm .git .gitignore .travis.yml

# Remove redundant project files
rm _Pod.xcodeproj
rm -rf Example/Podfile Example/Podfile.lock Example/Pods Example/ModuleName.xcworkspace

Step 5: Wire Module into Main Workspace

Edit your main Podfile to define the module pod and integrate its Example target:

def module_name_pod
    pod 'ModuleName', :path => 'DevPods/ModuleName'
end

# Move test targets OUTSIDE main app target
target 'App' do
    # Your app dependencies
    module_name_pod
end

target 'AppTests' do
    inherit! :search_paths
end

target 'AppUITests' do
end

# Enable isolated module development from main workspace
target 'ModuleName_Example' do
    use_frameworks!
    project 'DevPods/ModuleName/Example/ModuleName.xcodeproj'
    module_name_pod
end

Run pod install to generate the unified workspace.

Step 6: Migrate Source Files

Inside DevPods/ModuleName/ModuleName, replace Assets/Classes with a Module folder. Move your .swift, .xcassets, .storyboard, .xcdatamodeld files here—manually or via terminal, never through Xcode's group structure alone.

Update ModuleName.podspec:

s.ios.deployment_target = '15.0'
s.source_files = 'MoviesSearch/Module/**/*.{swift}'
s.resources = "MoviesSearch/Module/**/*.{xcassets,json,storyboard,xib,xcdatamodeld}"

Critical Note: .xcdatamodel must become .xcdatamodeld via Editor → Add Model Version.


REAL Code Examples from the Repository

The repository's README contains production-hardened code patterns. Here are the essential snippets with detailed explanations:

Example 1: Podfile Module Definition with Test Specs

# Define reusable pod reference with test specification
def module_name_pod
    # :path points to local development folder
    # :testspecs includes unit tests in main workspace
    pod 'ModuleName', :path => 'DevPods/ModuleName', :testspecs => ['Tests']
end

Before this pattern: Tests lived scattered in main app target, running slowly and coupling test infrastructure to app lifecycle.

After this pattern: Module tests execute in isolation via ModuleName-Unit-Tests scheme, yet remain accessible from main app workspace with Cmd + U. The :testspecs parameter instructs CocoaPods to generate a separate test bundle that Xcode's Test Navigator recognizes natively.

Example 2: Bundle-Aware Resource Loading

// MARK: - Image Loading from Module Bundle
// Critical: UIImage(named:) alone searches main bundle only—fails for pod resources
let image = UIImage(
    named: "image_name",
    in: Bundle(for: Self.self),  // Resolves to module's actual bundle
    compatibleWith: nil
)

// MARK: - Image Literal Support for Module Bundles
// Swift image literals default to main bundle; this wrapper redirects
final class LiteralBundleImage: _ExpressibleByImageLiteral {
    let image: UIImage?
    
    required init(imageLiteralResourceName name: String) {
        image = UIImage(
            named: name,
            in: Bundle(for: Self.self),  // Module-local resolution
            compatibleWith: nil
        )
    }
}

// Usage preserves literal syntax while fixing bundle resolution
let image = (#imageLiteral(resourceName: "image_name") as LiteralBundleImage)

// MARK: - Storyboard and Nib Loading
let storyboard = UIStoryboard(
    name: "name",
    bundle: Bundle(for: Self.self)
)
let nib = UINib(
    nibName: "name",
    bundle: Bundle(for: Self.self)
)

// MARK: - Core Data Model Loading from Module
// xcdatamodeld required; xcdatamodel alone fails at runtime
guard let modelURL = Bundle(for: Self.self).url(
        forResource: "ModelFileName",
        withExtension: "momd"  // Compiled model directory extension
    ),
    let mom = NSManagedObjectModel(contentsOf: modelURL)
else {
    fatalError("Unable to located Core Data model")
}
let container = NSPersistentContainer(
    name: "Name",
    managedObjectModel: mom  // Explicit model prevents automatic search failures
)

Why this matters: When CocoaPods packages modules as dynamic frameworks, resources don't merge into mainBundle. The Bundle(for:) pattern uses Objective-C runtime class lookup to locate the correct .framework container. Without this, every UIImage(named:), UIStoryboard(name:), and Core Data initialization silently returns nil in production—crashes that only appear after pod install, never in raw Xcode projects.

Example 3: Static Library Resource Bundle Conversion

# Dynamic framework resources (simpler but slower app launch)
s.resources = 'ModuleName/Module/**/*.{xcassets,json,storyboard,xib,xcdatamodeld}'

# STATIC LIBRARY conversion: resources become separate .bundle files
# Key: 'ModuleName' key becomes the bundle name referenced in code
s.resource_bundles = {
    'ModuleName' => ['ModuleName/Module/**/*.{xcassets,json,storyboard,xib,xcdatamodeld}']
}
// Runtime bundle resolution for static libraries
extension Bundle {
    var resource: Bundle {
        // resourceURL points to host app; append constructed bundle path
        return Bundle(url: resourceURL!.appendingPathComponent("ModuleName.bundle"))!
    }
}

// Replace all Bundle(for: Self.self) calls with:
Bundle(for: Self.self).resource

Performance insight: Static libraries eliminate dyld overhead at app launch—critical for apps targeting <2 second cold starts. The tradeoff is manual bundle resolution, but the template provides the exact extension pattern.

Example 4: CI/CD Pipeline with Fastlane

# fastlane/Fastfile
lane :test do |options|
    # PHASE 1: Verify module independence
    # all_modules_schemes is an array of module example schemes
    all_modules_schemes.each do |s|
        UI.message "Testing if module #{s} is buildable"
        scan(
            scheme: s,              # Build each module in isolation
            device: simulator,
            build_for_testing: true, # Compile only; skip test execution for speed
        )
    end
    
    # PHASE 2: Full integration verification
    scan(
        scheme: "App",      # Main app with all modules integrated
        device: simulator,  # Runs all unit and UI tests
    )
end
# .travis.yml
os: osx
osx_image: xcode11.2
language: swift
script:
  - fastlane test  # Single command runs complete verification pipeline

Pipeline strategy: The two-phase approach catches module-level compilation errors before expensive full-app test execution. If NetworkingService fails to build, the pipeline exits in 30 seconds instead of 15 minutes—massive CI cost savings at scale.


Advanced Usage & Best Practices

Protocol-Based Dependency Delegation

When a module needs unextracted functionality (like opening chat), don't force premature extraction. Define a protocol inside the module:

protocol ChatOpener {
    func openChat(forUserId: String, inView: UIViewController)
}

Implement in main App, inject via DIContainer. This boundary-first design lets modules declare needs without owning implementations.

Manual File Movement Discipline

Xcode's group structure lies. Always move files via Finder or terminal, then pod install to regenerate. The template warns repeatedly about this because Xcode's virtual groups desync from filesystem reality—leading to "file not found" build errors that waste hours.

Schema Configuration for Cmd+U Testing

After adding :testspecs, manually edit each module's Example scheme: Test → + → Pod → ModuleName-Unit-Tests. Without this, Xcode's test runner won't discover pod-hosted tests despite successful compilation.

Core Data Codegen Migration

Static library modules require Manual/None codegen with explicit NSManagedObject subclass generation. Automatic codegen places files in derived data that static linking can't resolve—another silent failure mode the template explicitly addresses.


Comparison with Alternatives

Approach Build Speed Learning Curve Isolation CI/CD Ready Best For
iOS Modular Architecture (CocoaPods) ⭐⭐⭐⭐ Medium ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Teams scaling past 5 engineers
Swift Package Manager (SPM) ⭐⭐⭐⭐⭐ Low ⭐⭐⭐⭐ ⭐⭐⭐⭐ Greenfield projects, Apple-centric
Tuist ⭐⭐⭐⭐⭐ High ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Complex workspace generation
Manual Framework Targets ⭐⭐⭐ Very High ⭐⭐⭐⭐ ⭐⭐⭐ Absolute control seekers
Single Monolith None Prototypes only

Why this template wins: SPM lacks mature local development pod workflows. Tuist requires learning a domain-specific language. Manual frameworks demand excessive Xcode project manipulation. This template hits the sweet spot of proven tooling, reasonable complexity, and production validation (OLX runs this at scale).


FAQ

Q: Can I use this with SwiftUI and modern Xcode versions? A: Absolutely. The CocoaPods infrastructure is version-agnostic. Update deployment targets to iOS 15+ and Swift 5.5+ in .podspec files. SwiftUI views migrate into modules identically to UIKit components.

Q: How do I handle shared UI components across modules? A: Extract a DesignSystem or UIComponents module at the dependency graph's foundation. Other modules declare s.dependency 'UIComponents' in their .podspec. Never let feature modules depend on each other directly.

Q: What's the performance impact of dynamic frameworks? A: Each dynamic framework adds ~50-100ms to cold launch. For apps with 15+ modules, convert to static libraries using the template's optional resource_bundles migration. The repository includes exact conversion steps.

Q: Can modules contain their own routing/navigation? A: Yes, but recommend coordinator pattern with protocol-defined interfaces. Modules expose navigation intents; main App's coordinator resolves actual view controller presentation. Keeps modules UI-framework-agnostic.

Q: How do I update a module's dependency version? A: Edit the module's .podspec s.dependency 'PromiseKit', '~> 6.0' constraint, then run pod update ModuleName from main project. Lock files propagate automatically.

Q: Is this overkill for solo developers? A: Start monolithic, extract when build times exceed 2 minutes or feature branches become painful. The template shines brightest with parallel development—but its test isolation benefits even single developers.

Q: Can I mix SPM and CocoaPods modules? A: Technically yes in modern Xcode, but the template doesn't demonstrate this. Hybrid approaches complicate dependency resolution. Choose one ecosystem per app unless forced by external requirements.


Conclusion

The iOS Modular Architecture template isn't just code—it's a survival manual for iOS teams outgrowing their own codebase. From the first pod lib create to running 20-module CI pipelines in under 5 minutes, every step has been production-hardened at OLX and shared generously with the community.

The monolith didn't break overnight, and your architecture won't heal instantly. But this template provides incremental extraction with guardrails: video guides, dependency visualizations, and CI configurations that prevent regression. Start with one service module. Feel the build speed improvement. Watch your team's parallel velocity multiply.

Your move: Clone the repository. Replace "Movie" with your domain. Extract your networking layer this sprint. Your future self—reviewing clean diffs at 5 PM instead of resolving merge conflicts at midnight—will thank you.

👉 Get the iOS Modular Architecture template on GitHub


Found this breakdown valuable? Star the repository, share with your iOS team lead, and subscribe for deeper architecture dives.

Advertisement

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Apps & Tools Open Source

Apps & Tools Open Source

Bright Coding Prompt

Bright Coding Prompt

Categories

Advertisement
Advertisement