tl;博士
如何在初始化期间创建一个能够从 特定位置 (不是来自框架的捆绑包)读取 .plist
的单例(它是框架的一部分)?
解决方案发布在下面,并基于接受的答案。
设置说明
我的 iOS 应用程序使用了一个专有的 UsefulKit.framework
,所有常用代码都在其中。
该框架有一个 ConfigurationManager
(单例),负责在初始化期间从 .plist
(RAII)加载一些设置(例如基本 URL、API key 等),并为其他有兴趣读取应用程序范围设置的组件提供 + (id)valueForKey:(NSString *)key;
API。ConfigurationManager
存储了一个默认名称 .plist
它期望在初始化期间加载(参见下面的问题 #3),即 EnvironmentConfiguration-Default.plist
。
管理器从 .plist
加载 [NSBundle bundleForClass:[self class]]
,并且在 管理器成为 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 注入(inject)捆绑包。事实上,我想不出任何一种“很好”的依赖注入(inject)。我错过了什么吗?也许是客户端应用程序分配路径的 static
var? EnvironmentConfiguration-Default.plist
的名称被硬编码到 ConfigurationManager
的内部,这对我来说很奇怪,b/c 其他开发人员必须知道它并进行设置,但是,我在许多 3rd 方框架(GoogleAnalytics、UrbanAirhip、Fabric ),框架期望在特定位置找到 .plist
(框架版本之间通常不同)。因此,开发人员应该阅读文档并准备环境作为框架集成的一部分。
欢迎任何有关更改架构的建议。
解决方案
以下内容高度基于 @NSGod 发布的建议,感谢!我将这种方法称为某种(静态?)依赖注入(inject)。
配置管理器.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
配置管理器.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
进行单元测试。
最佳答案
其实想一想,如果你想让框架和应用程序通信,你只需要修改一行代码:
- (void)loadDefaultEnvironmentConfiguration {
// NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSBundle* bundle = [NSBundle mainBundle];
当
ConfigurationManager
类是您的主应用程序的一部分,[NSBundle bundleForClass:[self class]]
返回主应用程序包(即 [NSBundle mainBundle]
返回的相同包。当您将 ConfigurationManager
类移动到框架(也可以视为包)时,[NSBundle bundleForClass:[self class]]
开始返回 NSBundle
对于您的框架而不是主应用程序包。当您调用
[NSBundle mainBundle]
在您的框架内,它将返回正在使用该框架的任何应用程序。或者,您可以使用类方法来设置将在初始化期间使用的默认值。
例如,在您的
ConfigurationManager
类公共(public)接口(interface):@interface ConfigurationManager : NSObject
+ (void)setDefaultConfigurationPath:(NSString *)aPath;
@end
在
ConfigurationManager.m
:static NSString *defaultConfigurationPath = nil;
@implementation ConfigurationManager
+ (void)setDefaultConfigurationPath:(NSString *)aPath {
@synchronized(self) {
defaultConfigurationPath = aPath;
}
}
// additional methods
- (void)loadDefaultEnvironmentConfiguration {
NSDictionary *dic = [NSDictionary
dictionaryWithContentsOfFile:defaultConfigurationPath];
//
// processing the plist file here...
//
}
@end
通过声明
defaultConfigurationPath
静态的,你让它成为一个“类”变量而不是一个实例变量。因此,您甚至可以在创建类的实例之前使用类方法来更改其值。我相信代码应该与 ARC 一样工作,尽管我并不肯定(我自己仍然习惯于手动引用计数)。您的主应用程序应确保
[ConfigurationManager setDefaultConfigurationPath:]
在任何人调用 [ConfigurationManager sharedInstance]
之前使用正确的路径调用.最好的地方是 +initialize
您的应用程序委托(delegate)的方法,这是最先被调用的方法之一:+ (void)initialize {
NSString *path; // get path for plist
[ConfigurationManager setDefaultConfigurationPath:path];
}
关于ios - 框架架构 : Specify NSBundle to load . 来自单例初始化期间的 plist 文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36505947/