Swift项目基于单一核心框架实现多Target/项目差异化UI的策略咨询
Hey there, this is a super common scenario when building multi-variant iOS apps— I’ve helped several teams implement this exact setup before. The core idea is to decouple your business logic completely from the UI layer, then use a single core framework + multiple targets + configuration to handle the differences. Here’s a step-by-step breakdown that works for projects heavy on XIBs/Storyboards:
1. Extract Core Business Logic into an Independent Framework
First, yank out all UI-agnostic core functionality (networking, data processing, business rules, utility classes) into a standalone CoreFramework. This framework should have zero dependencies on UIKit/AppKit— it only exists to serve data and enforce business rules.
- Both of your new targets will depend on this framework, ensuring 100% consistency in core features without duplicate code.
- This also makes it easier to test core logic independently of UI variants.
2. Split Your Project with Multiple Targets
Create two new targets (e.g., AppVariantA and AppVariantB) in your existing Xcode project:
- Each target links against
CoreFrameworkand gets its ownInfo.plist, app icons, launch screens, and bundle ID. - Use Xcode’s Build Phases to control which resources (images, fonts, XIBs) are included in each target— this is key for keeping variant-specific assets separate.
3. Abstract & Differentiate UI Components (The Heart of the Solution)
This is where you’ll handle different headers, cells, colors, and assets. The goal is to make your core logic depend on abstractions, not concrete UI implementations.
3.1 Define Protocols for UI Components
Start by creating protocols for any UI element that needs variant-specific behavior. For example:
protocol AppHeaderProtocol: UIView { func configure(with data: HeaderViewModel) func updateNotificationBadge(count: Int) } protocol CustomFeedCellProtocol: UITableViewCell { func setup(with model: FeedItemModel) }
Your core view controllers will only reference these protocols, so they don’t care which variant’s UI is being used.
3.2 Build Variant-Specific UI Implementations
For each target, create concrete classes that conform to these protocols:
- For XIB-based components: Make
VariantAHeaderView.xib+VariantAHeaderView.swift(conforming toAppHeaderProtocol), andVariantBHeaderView.xib+VariantBHeaderView.swiftfor the other variant. - Add each variant’s XIB/classes to their respective target’s Build Phases (don’t accidentally include Variant A’s files in Variant B’s target!).
3.3 Load the Right UI Component at Runtime
You have two solid options here:
Option 1: Use Compilation Macros
Define target-specific macros in each target’s Build Settings > Preprocessor Macros (e.g., VARIANT_A=1 for Variant A, VARIANT_B=1 for Variant B). Then switch implementations in code:
func makeHeaderComponent() -> AppHeaderProtocol { #if VARIANT_A return Bundle.main.loadNibNamed("VariantAHeaderView", owner: nil)?.first as! VariantAHeaderView #elseif VARIANT_B return Bundle.main.loadNibNamed("VariantBHeaderView", owner: nil)?.first as! VariantBHeaderView #endif }
This is simple and fast, but requires code changes if you add more variants later.
Option 2: Use a Configuration File
Create a VariantConfig.plist for each target, with keys like HeaderComponentClassName and PrimaryColorHex. Load this config at runtime and use reflection to instantiate components:
// Load config from bundle guard let configPath = Bundle.main.path(forResource: "VariantConfig", ofType: "plist"), let config = NSDictionary(contentsOfFile: configPath) as? [String: String] else { fatalError("Missing variant config!") } // Instantiate header component let headerClassName = config["HeaderComponentClassName"]! guard let headerClass = NSClassFromString(headerClassName) as? AppHeaderProtocol.Type else { fatalError("Invalid header component class!") } let headerView = headerClass.init()
This is more flexible— you can add new variants just by creating a new config file and target, no code changes needed.
4. Handle Storyboard/XIB Differences
If your project uses a lot of Storyboards:
- Create variant-specific Storyboards (e.g.,
MainVariantA.storyboardandMainVariantB.storyboard). Update each target’sInfo.plistto setMain storyboard file base nameto the correct file. - For shared view controllers, keep their logic in
CoreFrameworkand reference variant-specific UI components (like custom cells) directly in each Storyboard. - Or, add a helper extension to load the right Storyboard dynamically:
extension UIStoryboard { static func main() -> UIStoryboard { #if VARIANT_A return UIStoryboard(name: "MainVariantA", bundle: .main) #else return UIStoryboard(name: "MainVariantB", bundle: .main) #endif } }
5. Unify Variant-Specific Resources (Colors, Images, Fonts)
Keep your assets organized and easy to switch:
- Colors: Add an extension to
UIColorthat returns variant-specific colors using macros or config:
extension UIColor { static var primaryBrandColor: UIColor { #if VARIANT_A return UIColor(red: 0.12, green: 0.56, blue: 1.0, alpha: 1.0) #else return UIColor(red: 0.95, green: 0.26, blue: 0.21, alpha: 1.0) #endif } }
- Images: Name assets with a variant prefix (e.g.,
variantA_home_icon,variantB_home_icon) and use a helper extension to load the right one:
extension UIImage { static func variantImage(named baseName: String) -> UIImage? { #if VARIANT_A return UIImage(named: "variantA_\(baseName)") #else return UIImage(named: "variantB_\(baseName)") #endif } }
6. Optimize Testing & Builds
- Create separate schemes for each target so you can quickly switch between variants in Xcode.
- Write UI tests for each variant to ensure their unique components work as expected.
- If you plan to add more variants later, consider using Swift Package Manager or CocoaPods to package
CoreFrameworkfor easier reuse.
内容的提问来源于stack exchange,提问作者Pablo Blanco

