【问题标题】:Framework Architecture: Specify NSBundle to load .plist file from during singleton init框架架构:指定 NSBundle 以在单例初始化期间加载 .plist 文件
【发布时间】: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 所在的那个包。所以,我开始怀疑我在架构上做错了什么。

问题

  1. 由于 ConfigurationManager 使用单例模式构建,我无法通过 Constructor Injection 注入包。事实上,我想不出任何一种“很好”的依赖注入。我错过了什么吗?可能是客户端应用分配路径的static var?

  2. 框架是否可以在内部搜索其他捆绑包?

  3. 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


【解决方案1】:

其实想一想,如果你想让框架和应用程序通信,你只需要改变一行代码:

- (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 类公共接口中:

@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 sharedInstance] 之前使用正确的路径调用[ConfigurationManager setDefaultConfigurationPath:]。最好的方法是在您的应用委托的 +initialize 方法中,这是最先被调用的方法之一:

+ (void)initialize {
   NSString *path; // get path for plist
   [ConfigurationManager setDefaultConfigurationPath:path];
}

【讨论】:

  • static var 是我想到的可能解决方案之一。拥有一个公共二传手是对这个想法的一个很好的扩展。我还在寻找框架类以某种方式访问​​应用程序包的可能性。谢谢。
  • 非常感谢您的帮助。我刚刚发布了我所采用的解决方案,该解决方案高度基于您的建议。谢谢!
猜你喜欢
  • 2018-01-10
  • 1970-01-01
  • 1970-01-01
  • 2011-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-29
  • 1970-01-01
相关资源
最近更新 更多