Mobile Development 32 min read

Principles and Practices of iOS App Architecture

This article shares the author's insights on designing robust iOS app architecture, covering problem identification, module classification, dependency management, layering, common folder pitfalls, and practical steps for building maintainable, testable, and scalable mobile applications.

Art of Distributed System Architecture Design
Art of Distributed System Architecture Design
Art of Distributed System Architecture Design
Principles and Practices of iOS App Architecture

Origin

Most of the second‑generation architecture of the Anjuke iOS app was designed by me, and I later summarized some experiences. About a year later, a former colleague posted a question on WeChat Moments: if you were asked to describe the architecture of an iOS or Android app, what aspects would you discuss?

I answered the question on a bus ride home. After a few follow‑up questions from the colleague, I felt it was necessary to write an article to share my personal views.

In fact, the complexity of a client‑side iOS application is comparable to that of a server, but the focus and entry points differ. For example, a client app does not need to consider C10K‑type problems; a normal app never faces that.

This series will focus on iOS app architecture, with many solutions built around the characteristics of the iOS tech stack. I am not fond of Java, so I will not cover Android in depth; Android developers can still benefit from the architectural ideas presented here.

What do we discuss when talking about client‑side app architecture?

Most apps on the market essentially perform the following cycle repeatedly:

In plain terms: call an API, display a page, jump elsewhere, call another API, and display another page.

What is there to architect?

Non‑non, non‑non. — Excerpt from "The Deer and the Cauldron"

While an app mainly performs the above actions, the underlying foundation that supports these actions is what architecture must consider:

Network API calls : How to let business engineers call APIs safely and conveniently while ensuring a good user experience under various network conditions?

Page presentation : How to organize pages to minimize coupling for business code, reduce UI development complexity, and improve efficiency?

Local data persistence : When data needs local storage, how to arrange it reasonably and keep performance overhead low?

Dynamic deployment : iOS apps have review cycles; how to deliver new content or fix urgent bugs without releasing a new version?

In addition to the app‑level concerns, there are team‑level concerns:

Collecting user data to provide references for product and operations.

Organizing business modules and related foundational modules in a reasonable way.

Automating daily app builds and providing testing tools for QA engineers.

These three points are just a subset; many more exist.

What questions will this series answer?

Network layer design: what issues to consider and where to optimize?

Page display, invocation, and organization: design options and considerations.

Local persistence layer design: options, pros/cons, and caveats.

Dynamic deployment solutions: options, trade‑offs, and focus points.

What questions does this article answer?

The four questions above will each be covered in separate articles. This article serves as a general introduction and opens the floor for discussion of foundational issues.

Architecture design methodology

When you start designing and implementing a layer or the whole app architecture, you may feel stuck. The following methodology is distilled from years of experience. Every architect has their own method, but common elements include a global view, strong code aesthetics, and flexible use of design patterns.

Step 1: Identify the problems to solve and their necessary conditions.

You must understand what the business wants. Do not architect for the sake of architecture, nor adopt new technologies just for novelty. If MVC works well and has no major flaws, there is no need to replace it with MVVM.

Sometimes system‑provided functions need extra parameters (e.g., read needs a file descriptor, buffer, and size). For the business side, the essential condition may be just the file descriptor.

Similarly, pagination may only require a loadNextPage method for the business, not the current page number.

Clarifying the true necessary conditions for the business reduces coupling and makes future module replacement cheaper.

Step 2: Classify problems and split into modules.

This step is self‑explanatory.

Step 3: Understand dependencies between problems, establish module communication standards, and design modules.

Establish a unified communication protocol. This reflects the architect’s software values. While there are debates (e.g., fat Model vs. skinny Model), a competent architect should avoid obviously bad designs.

Maintain a single set of standards; multiple conflicting standards lead to maintenance disasters.

Step 4: Predict future directions, add new modules when needed, and record baseline data for future needs.

A good architecture serves the present and remains adaptable for the future; software has a lifecycle.

Step 5: Solve the most fundamental dependency problems first, implement core modules, then stack the rest of the architecture.

This step validates earlier designs; adjustments may be required as implementation proceeds.

Step 6: Instrument, run unit and performance tests, and optimize based on data.

Use the data to demonstrate value to leadership and continuously refine the architecture.

In summary, follow these principles: top‑down design (steps 1‑4), bottom‑up implementation (step 5), then measure and optimize (step 6).

What makes a good architect?

Continuously learns; quickly grasps new technologies and ideas. Without this, you are just a coder.

Has a business background or deep familiarity with the company’s domain. Without this, you are more of an operations person.

Knows software engineering standards, avoids quick‑and‑dirty shortcuts, and has fallen into many pitfalls.

Admits mistakes promptly; owning errors does not diminish architectural credibility.

Does not chase flashy techniques for their own sake.

Pursues excellence.

What makes a good architecture?

Clean code, clear classification, no vague "common" or "core" folders.

Minimal or no documentation needed for business teams to get started.

Unified thinking and methods; avoid heterogeneous solutions.

No horizontal dependencies; avoid cross‑layer access unless absolutely necessary.

Appropriate constraints and flexibility for business needs.

Easy to test and extend.

Some degree of forward‑looking technology adoption.

Few interfaces with few parameters.

High performance.

Explanation of selected points:

Clean code, clear classification, no "common" or "core". Disorganized code hampers maintenance. Modules should have a single responsibility; avoid mixing unrelated responsibilities.

Little or no documentation. Business teams are busy; make API names self‑descriptive. In Objective‑C, long method names are acceptable.

Good function name example:

- (NSDictionary *)exifDataOfImage:(UIImage *)image atIndexPath:(NSIndexPath *)indexPath;

Bad function name example:

- (id)exifData:(UIImage *)image position:(id)indexPath callback:(id<ErrorDelegate>)delegate;

Why the bad example is problematic:

Avoid returning or accepting generic id ; be specific.

Explicitly describe required inputs (e.g., ofImage , atIndexPath ).

Do not pass a delegate unless it is truly a required condition; its presence often signals a design flaw.

Unified thinking and methods. Once a solution is chosen, apply it consistently across the codebase; record the rationale.

No horizontal dependencies. Reduces coupling and future modification cost.

Balanced constraints and flexibility. Example: Core Data’s managed objects should be encapsulated behind a limited API to prevent uncontrolled modifications.

AB‑test API design should allow both target‑action and block‑based usage to give business flexibility.

Easy testing and extensibility. High modularity and low coupling enable mocking and straightforward expansion.

Forward‑looking. Stay aware of industry trends and product roadmaps; leave room for anticipated changes.

Few interfaces, few parameters. Satisfy essential conditions while minimizing surface area.

Performance. Important but not the primary concern for client apps; iOS hardware is powerful, and performance gains are often imperceptible to users.

About layering?

Common layering models include three‑layer (presentation, business, data) and four‑layer (presentation, business, network, local data). These are logical classifications, not strict standards like TCP/IP layers.

Often MVC or MVVM is described in terms of data flow rather than module classification; they can coexist (e.g., MVC with four logical layers).

Historically, three‑layer architecture became popular because most modules fall into three roles: data manager, data processor, and data presenter. Additional layers are usually subdivisions of these core roles.

When deciding on layers, first identify all problems, then list modules, and finally group them; most designs naturally resolve into three layers unless a layer becomes too large.

Example of server‑side layering for an instant‑messaging service is provided to illustrate the process, though the principles apply equally to client architecture.

About the "Common" folder?

Comments from a reviewer noted that a "Common" folder is just a folder, but it often becomes a dumping ground for miscellaneous code, leading to tangled dependencies.

Typical scenario: small utility classes (e.g., location, image processing) are placed in a "Common" module or pod. As the project grows, these utilities expand, and the "Common" module becomes a monolithic, hard‑to‑maintain entity.

Problems with a large "Common": Encourages unchecked cross‑module dependencies. Violates fine‑grained modular design principles. Creates heavy maintenance overhead; any change may affect many unrelated features.

Benefits of avoiding a "Common" folder: Forces engineers to consider dependencies early, leading to well‑scoped modules. Reduces app size by excluding unnecessary code. Improves maintainability and simplifies module upgrades. Aligns with fine‑grained modular architecture.

Recommendation: even a tiny module (e.g., Location, ImageProcess) should have its own dedicated module rather than being placed in a generic "Common" folder. This keeps the project clean and makes future scaling easier.

design patternsmobile developmentiOSModularizationApp Architecture
Art of Distributed System Architecture Design
Written by

Art of Distributed System Architecture Design

Introductions to large-scale distributed system architectures; insights and knowledge sharing on large-scale internet system architecture; front-end web architecture overviews; practical tips and experiences with PHP, JavaScript, Erlang, C/C++ and other languages in large-scale internet system development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.