Mobile Development 14 min read

How NetEase Cloud Music Made Objective‑C Components Module‑Ready for Swift Integration

This article details the challenges and step‑by‑step solutions the NetEase Cloud Music iOS team used to modularize legacy Objective‑C libraries, enabling seamless Swift interop through modulemap generation, CocoaPods configuration, and custom build scripts while addressing macro, header, and static‑library issues.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
How NetEase Cloud Music Made Objective‑C Components Module‑Ready for Swift Integration

Background

NetEase Cloud Music's iOS app has evolved over years, accumulating a large Objective‑C codebase. To integrate new Swift‑based features, the team needed to make existing Objective‑C components module‑compatible, which requires each component to expose a .modulemap file.

What Are Modules?

Apple introduced Modules in 2012 to improve the scalability and stability of C‑family languages. A module describes a component’s public interface via a .modulemap file, allowing Clang to locate headers, compile the module once, and cache the result.

AFramework.framework
├─ Headers
├─ Info.plist
├─ Modules
│   └─ module.modulemap
└─ AFramework

Enabling Modules

In Xcode, set Defines Module to YES under Build Settings. When using CocoaPods, modules can be enabled in several ways:

Add use_modular_headers! to the Podfile to enable modules for all pods.

Set :modular_headers => true for individual pods.

In the podspec, add s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }.

Use use_frameworks! :linkage => :static to build static frameworks that automatically include a modulemap.

The first three methods affect static library builds ( .a), while the fourth works for framework builds.

Current Moduleization Status

The project uses CocoaPods to integrate dependencies, and most libraries have been static‑linked into frameworks without module support. Simply turning on the module flag often leads to compilation failures because:

Modules have transitive dependencies; enabling a module requires all its dependencies to be modular as well, which is unlikely in a large codebase.

Legacy code contains unconventional macros, PCH‑based implicit dependencies, and other patterns that break module compilation.

Moduleization Strategies

Components fall into three categories:

Module Framework – built with Defines Module enabled; no extra work needed.

Non‑Module Framework – compiled as a static library, then a script generates a .modulemap and places it in Framework/Modules.

.a Static Library – lacks a modulemap; must be wrapped in a framework or have a dummy Swift file to trigger modulemap generation.

For non‑module frameworks, the team adds a custom .modulemap that points to an umbrella header. Clang first looks for the header in the framework’s Headers directory, then checks Modules for the modulemap.

// NMSetting.framework/Modules/NMSetting.modulemap
framework module NMSetting {
  umbrella header "NMSetting-umbrella.h"
  export *
  module * { export * }
}

This “cheats” the compiler into treating the static library as a module without requiring its dependencies to be modular.

Typical Moduleization Problems and Solutions

Missing Macro Definitions

Swift cannot import Objective‑C macros directly; they are converted to global constants only for simple literals. Complex macros must be wrapped in Objective‑C code and exposed via functions or constants.

#define MAX_RESOLUTION 1268
#define HALF_RESOLUTION (MAX_RESOLUTION / 2)

Converted to Swift:

let MAX_RESOLUTION = 1268
let IS_HIGH_RES = 634

When macros are used via #include, module compilation hides them because #include no longer performs textual substitution. The fix is to add the missing headers to the modulemap as textual header entries.

framework module FrameworkName {
  umbrella header "FrameworkName-umbrella.h"
  textual header "ItemList.h"
  export *
  module * { export * }
}

Missing Header Files (PCH Issues)

Many components rely on a precompiled header (PCH) that implicitly provides imports. When modules are enabled, the implicit PCH is ignored, causing header‑not‑found errors. The team resolved this by explicitly adding the missing headers to the modulemap or by making the headers private in the podspec to avoid exposure.

Static .a Libraries

Static libraries lack a modulemap. Two low‑cost solutions were used:

Inject an empty .swift file into the library directory and specify source_files and swift_version in the podspec; CocoaPods then generates a modulemap automatically.

Use a CocoaPods plugin during the pre_install phase to set pod_target.should_build, forcing CocoaPods to create a modulemap for the static library.

The team chose the second approach for its lower overhead.

Conclusion

Moduleizing Objective‑C components is essential for Swift interop. The core task is providing a valid .modulemap. While many issues arise from macro handling, missing headers, and static library packaging, a combination of custom modulemaps, CI automation, and selective CocoaPods configuration can resolve them.

Future Plans

Prevent module degradation by enabling modules early in local development and enforcing checks in CI.

Refactor Objective‑C APIs for Swift compatibility, addressing safety and usability concerns.

Standardize header imports and reject non‑conforming code during code review.

iOSCocoaPodsSwiftObjective‑CBuild SystemModulesInterop
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

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.