【发布时间】:2016-04-21 20:51:17
【问题描述】:
tl;dr
如何创建一个能够在初始化期间从特定位置(不是来自框架的捆绑包)读取.plist 的单例(框架的一部分)?
解决方案发布在下面并基于接受的答案。
设置说明
我的 iOS 应用程序使用专有的UsefulKit.framework,所有常用代码都在其中。
框架有一个ConfigurationManager(Singleton),负责在初始化(RAII)期间从.plist加载一些设置(例如Base URL,API Keys等),并提供+ (id)valueForKey:(NSString *)key; API给其他有兴趣读取应用程序范围设置的组件。
ConfigurationManager 存储一个默认名称 .plist,它期望在初始化期间加载(参见下面的问题 #3),即 EnvironmentConfiguration-Default.plist。
经理从[NSBundle bundleForClass:[self class]] 加载.plist,它曾经工作正常之前经理成为UsefulKit.framework 的一部分。当它是主应用程序的一部分时,它在同一个包中有各自的.plist,并且能够通过名称找到它。请参阅下面ConfigurationManager.m 的代码。
NSString * const kDefaultEnvironmentConfigurationFileName = @"EnvironmentConfiguration-Default";
@interface ConfigurationManager ()
@property (nonatomic, strong) NSMutableDictionary *environmentInfo;
@end
@implementation ConfigurationManager
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
- (instancetype)init {
self = [super init];
if (self) {
self.environmentInfo = [NSMutableDictionary new];
[self loadEnvironment];
}
return self;
}
- (void)loadEnvironment {
[self.environmentInfo removeAllObjects];
[self loadDefaultEnvironmentConfiguration];
}
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSString *defaultPlistPath = [bundle pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
assert(defaultPlistPath != nil); // <=== code crashes here
//
// processing the plist file here...
//
}
// ...
// some code omitted
// ...
@end
问题
现在,当它是UsefulKit.framework 的一部分时,该方法不起作用。只要我将EnvironmentConfiguration-Default.plist 与框架捆绑在一起,它就可以工作,我不想这样做,因为可能使用框架的应用程序之间的配置不同。应用程序必须有各自的.plist 并使用框架的ConfigurationManager 来访问设置。
此代码也不适用于框架的 Xcode 项目中的单元测试目标。我将EnvironmentConfiguration-Default.plist 文件放入测试目标包并编写了这个单元测试:
- (void)testConfigurationManagerInstantiation {
[ConfigurationManager sharedInstance];
}
...代码在-loadDefaultEnvironmentConfiguration 崩溃(见上文)。
调试上述方法我看到了这个:
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
// Printing description of bundle:
// NSBundle </Users/admin/Library/Developer/Xcode/DerivedData/MyWorkspace-asazpgalibrpubbrimxpbrebqdww/Build/Products/Debug-iphonesimulator/UsefulKit.framework> (loaded)
NSString *defaultPlistPath = [[NSBundle bundleForClass:[self class]] pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
// Printing description of defaultPlistPath:
// <nil>
这个包绝对不是我的.plist 所在的那个包。所以,我开始怀疑我在架构上做错了什么。
问题
由于
ConfigurationManager使用单例模式构建,我无法通过 Constructor Injection 注入包。事实上,我想不出任何一种“很好”的依赖注入。我错过了什么吗?可能是客户端应用分配路径的staticvar?框架是否可以在内部搜索其他捆绑包?
EnvironmentConfiguration-Default.plist的名称被硬编码到ConfigurationManager的内部,这对我来说很臭,b/c 其他开发人员必须知道它并进行设置,但是,我看到很多人都看到了这种事情3rd 方框架(GoogleAnalytics、UrbanAirhip、Fabric),框架期望在特定位置找到.plist(框架版本之间通常不同)。因此,开发人员应阅读文档并准备环境作为框架集成的一部分。
欢迎任何有关更改架构的建议。
解决方案
以下内容高度基于@NSGod 发布的建议,不胜感激!我将这种方法称为某种(静态?)依赖注入。
ConfigurationManager.m:
static NSBundle * defaultConfigurationBundle = nil;
@implementation ConfigurationManager
+ (void)initialize {
if (self == [ConfigurationManager class]) {
/// Defaults to main bundle
[[ConfigurationManager class] setDefaultConfigurationBundle:[NSBundle mainBundle]];
}
}
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle {
@synchronized(self) {
defaultConfigurationBundle = bundle;
}
}
// ...
@end
ConfigurationManager.h:
@interface ConfigurationManager : NSObject
// ...
/**
@brief Specify default NSBundle, other than [NSBundle mainBundle] (which is used, otherwise) where .plist configuration file is expected to be found during initialization.
@discussion For some purpose (e.g. unit-testing) there might be cases, where forcing other NSBundle usage is required. The value, assigned in this method might be [NSBundle bundleForClass:[self class]], to get the bundle for caller.
@attention This method must be called before any other method in this class for assignment to take effect, because default bundle setup happens during class instantiation.
@param An NSBundle to read Default .plist from.
*/
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle;
// ...
@end
在呼叫现场:
@implementation ConfigurationManagerTests
- (void)setUp {
[super setUp];
/// Prepare test case with correct bundle
[ConfigurationManager setDefaultConfigurationBundle:[NSBundle bundleForClass:[self class]]];
}
- (void)testConfigurationManagerInstantiation {
// call sequence:
// 1. +initialize
// 2. +setDefaultConfigurationBundle
// 3. +sharedInstance
XCTAssertNoThrow([ConfigurationManager sharedInstance]);
}
// ...
@end
该方法允许从应用程序目标简化框架的使用(mainBundle 是 .plist 所在的位置),因此到目前为止,只有单元测试需要 +setDefaultConfigurationBundle。
【问题讨论】:
-
.framework所在的ConfigurationManager类的名称是什么?是UsefulKit.framework吗?如果是这样,您是否将EnvironmentConfiguration-Default.plist文件添加到该UsefulKit.framework项目,并将其添加到UsefulKit.framework目标的复制资源构建阶段?换句话说,你确定EnvironmentConfiguration-Default.plist文件存在于UsefulKit.framework/Resources/EnvironmentConfiguration-Default.plist吗? -
ConfigurationManager中的[NSBundle bundleForClass:[self class]];代码将为找到ConfigurationManager类的框架返回NSBundle(即UsefulKit.framework?)。我很困惑为什么你说“将 EnvironmentConfiguration-Default.plist 文件放入测试目标包并编写这个单元测试......”。它是需要该配置文件的框架... -
@NSGod
UsefulKit.framework是一个框架名称。ConfigurationManager是框架的一部分。关键是,我不希望EnvironmentConfiguration-Default.plist成为框架的一部分。它必须是客户端应用程序的一部分。管理器必须接受配置并使用方便的 API。我不知道如何做到这一点。 -
@NSGod 谢谢你的问题,这让我觉得问题不清楚。现已编辑。
标签: ios objective-c unit-testing architecture ios-frameworks