ios - 框架架构 : Specify NSBundle to load . 来自单例初始化期间的 plist 文件

标签 ios objective-c unit-testing architecture ios-frameworks

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/

    相关文章:

    iOS希望在按下按钮时延迟 View Controller 之间的连接

    ios - 如何从CloudKit更新所有客户记录?

    ios - 在 Swift 中使用标签

    ios - 本地通知覆盖iOS 9中的远程通知(可行通知)设置?

    ios - 应用程序的 Info.plist 必须包含 NSMotionUsageDescription 键

    iphone - 如何以编程方式更改应用程序语言而不重新启动我的应用程序?

    unit-testing - 单元测试难题

    ios - 将砌体 x/y 约束设置为 View 宽度/高度的百分比

    php - 运行 PHPUnit 时出错

    unit-testing - 使用 Mockito 模拟 Akka Actor 日志对象