Part three of the microapps series moves resources and translations into the modules that use
them. The compiler generates a bundle per module with resources, reachable as Bundle.module,
and forgetting the bundle parameter is the classic missing-image bug.
Module Bundles
Image("image") looks in the running target's bundle, so a resource that lives in a package
module needs .module spelled out. Xcode auto-bundles asset catalogs, Core Data models,
Storyboards, NIBs, and localization files; Bundle.module exists only for modules that contain
resources. Wrap the lookup in a type-safe factory so feature modules cannot mistype names:
// DesignSystem module
extension UIImage {
public enum Icon: String {
case trash
case star
case plus
}
public static func get(_ icon: Icon) -> UIImage {
.init(named: icon.rawValue, in: .module, compatibleWith: .current)!
}
}
Other file types must be declared in Package.swift. process lets Xcode apply
platform-specific compression; copy keeps files and sub-directory structure verbatim.
.target(
name: "DesignSystem",
resources: [
.copy("Resources/Fonts"),
.process("Resources/PNGs")
]
)
Localization per Feature Module
Each feature module carries its own Localizable.strings, so it can build and run as a
standalone microapp. Set defaultLocalization: "en" on the package, add en.lproj and other
<locale>.lproj folders inside the module, and always pass the bundle when reading a string:
Text("helloMessage", bundle: .module)
A shared view that displays localized text from other modules must accept the bundle in its public initializer, because otherwise it can only see its own strings:
// DesignSystem module
public struct PlaceholderView: View {
let text: LocalizedStringKey
let bundle: Bundle?
let systemImage: String
public init(_ text: LocalizedStringKey, bundle: Bundle? = nil, systemImage: String) {
self.text = text
self.bundle = bundle
self.systemImage = systemImage
}
public var body: some View {
Label {
Text(text, bundle: bundle)
} icon: {
Image(systemName: systemImage)
}
}
}
// Search feature module
PlaceholderView("emptySearchPlaceholder", bundle: .module, systemImage: "magnifyingglass")
Two rules keep the separation clean: a resource lives only in the module that uses it, and shared resources get a type-safe public API.