railsware / BloodMagic

BloodMagic is a framework, which gives you a way to create custom property attributes.
MIT License
316 stars 36 forks source link

BloodMagic

License MIT   Build status   Version   Platform

Objective-C is a powerful language, but sometimes it lacks of custom property attributes, like these:

@property (nonatomic, strong, lazy) ProgressViewService *progressView;
@property (nonatomic, strong, partial) HeaderView *headerView;
@property (nonatomic, strong, final) NSString *almostImmutable;
@property (nonatomic, strong, preference) NSString *authToken;
@property (nonatomic, strong, injectable) id<NetworkClient> client;

@property (nonatomic, strong, anything_you_want) AwesomeView *someAwesomeView;

We can't implement these attributes without hacking on clang, but fortunately, we're able to achieve these effects by means of BloodMagic' spells

FAQ

Blog-post

Presentation by AlexDenisov

Presentation by Ievgen Solodovnykov

Embark on the Dark

CocoaPods:

  pod 'BloodMagic', :git => 'https://github.com/railsware/BloodMagic.git'

Components

$ mkdir -p ./Components.make
# iOS
wget https://raw.githubusercontent.com/AlexDenisov/Components/master/Components.make/BloodMagic/1.0.0/BloodMagic-iOS.make -O ./Components.make/BloodMagic-iOS.make
# OSX
wget https://raw.githubusercontent.com/AlexDenisov/Components/master/Components.make/BloodMagic/1.0.0/BloodMagic-OSX.make -O ./Components.make/BloodMagic-OSX.make

Manually

Alternatively you can use built frameworks for iOS and OSX.

Just drag&drop framework into your project and don't forget to add -all_load, -ObjC and -lc++ or -lstdc++ to OTHER_LINKER_FLAGS

Available Spells

Lazy Initialization

Dependency Injection

Partial Views

Assign-once properties

Preferences (NSUserDefaults wrapper)

BloodMagic has been designed to be extensible, so few more spells will be available soon.

====

Lazy initialization

  pod 'BloodMagic/Lazy', :git => 'https://github.com/railsware/BloodMagic.git'

Initializes object on demand.

If you use Objective-C, then you should be familiar with this code:

@interface ViewController : UIViewController

@property (nonatomic, strong) ProgressViewService *progressViewService;

@end
- (ProgressViewService *)progressViewService
{
    if (_progressViewService == nil) {
      _progressViewService = [ProgressViewService new];
    }

    return _progressViewService;
}

But we are able to automate this routine!

Just add BMLazy protocol to your class:

@interface ViewController : NSObject
  <BMLazy>

@property (nonatomic, strong, bm_lazy) ProgressViewService *progressViewService;

@end

and mark any property as @dynamic:

@implementation ViewController

@dynamic progressViewService;

@end

Object progressViewService will be initialized on the first call

self.progressViewService
// or
yourViewController.progressViewService

or when you try to get value for key

[self valueForKey:@"progressViewService"]
// or
[yourViewController valueForKey:@"progressViewService"]

By default it creates an instance with the +new class' method.

In this case progressViewService will be deallocated as a usual property.

Dependency Injection

  pod 'BloodMagic/Injectable', :git => 'https://github.com/railsware/BloodMagic.git'

During the creation of Lazy Initialization spell an interesting side effect was found - Dependency Injection.

It behaves the same way as BMLazy, but uses another approach to instantiate object.

For example, if you need to initialize progressViewService in a special way, you should provide initializer:

BMInitializer *initializer = [BMInitializer injectableInitializer];
initializer.propertyClass = [ProgressViewService class]; // optional, uses NSObject by default
initializer.containerClass = [ViewController class]; // optional, uses NSObject by default
initializer.initializer = ^id (id sender){
    return [[ProgressViewService alloc] initWithViewController:sender];
};
[initializer registerInitializer];

Note: containerClass doesn't apply on derived classes, to achieve such behavior you should specify containerClass explicitly.

This spell is very useful when dealing with the singleton

BMInitializer *initializer = [BMInitializer injectableInitializer];
initializer.propertyClass = [RequestManager class];
initializer.initializer = ^id (id sender){
    static id singleInstance = nil;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
      singleInstance = [RequestManager new];
    });
    return singleInstance;
};
[initializer registerInitializer];

Thus, neither the RequestManager nor the class that uses it, will not be aware about his singleton nature.

Adepts of SRP school must approve ;)

Also, you're able to use @protocols as well

BMInitializer *initializer = [BMInitializer injectableInitializer];
initializer.protocols = @[ @protocol(ProgressViewServiceProtocol) ];
initializer.initializer = ^id (id sender){
    return [[ProgressViewService alloc] initWithViewController:sender];
};
[initializer registerInitializer];
Injection hooks

BMInjectable module provides a hook system to catch the object creation. To enable these hooks just create instance method named propertyNameInjected:.

For example:

@implementation ViewController

@injectable(progressViewService)

- (void)progressViewServiceInjected:(ProgressViewService *service)
{
    service.title = self.title;
}

@end

Partial Views

  pod 'BloodMagic/Partial', :git => 'https://github.com/railsware/BloodMagic.git'

Instantiates view from xib on demand, similar to Lazy module. This spell might be helpful if you have reusable views.

For example:

You need to show the same user info in table cells (UsersListViewController) and in some header view (UserProfileViewController). It makes sense to create one UserView.xib associated with UserView class and use it through the whole app.

So it may looks like this:


// Cell used from UsersListViewController
// Created manually
@implementation UserViewCell
{
    UserView *_userView;
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        NSString *nibName = NSStringFromClass([UserView class]);
        UINib *nib = [UINib nibWithNibName:nibName bundle:nil];
        _userView = [[nib instantiateWithOwner:nil options:nil] lastObject];
        [self addSubview:_userView];
    }
    return self;
}

@end

// View used from UserProfileViewController
// Created from xib
@implementation UserHeaderView
{
    UserView *_userView;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    NSString *nibName = NSStringFromClass([UserView class]);
    UINib *nib = [UINib nibWithNibName:nibName bundle:nil];
    _userView = [[nib instantiateWithOwner:nil options:nil] lastObject];
    [self addSubview:_userView];
}

@end

Both cases use the same, similar code. So, BloodMagic does nothing special, just hides this boilerplate:


#import <BloodMagic/Partial.h>

@interface UserViewCell ()
    <BMPartial>

@property (nonatomic, strong, bm_partial) UserView *userView;

@end

@implementation UserViewCell

@dynamic userView;

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self addSubview:self.userView];
    }
    return self;
}

@end

// ...

@interface UserHeaderView ()
    <BMPartial>

@property (nonatomic, strong, bm_partial) UserView *userView;

@end

@implementation UserHeaderView

@dynamic userView;

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self addSubview:self.userView];
}

@end

Assign-once properties

  pod 'BloodMagic/Final', :git => 'https://github.com/railsware/BloodMagic.git'

Java provides final keyword, which determines (at least) that value can't be changed after initialization.

From now this feature available in Objective-C, via BloodMagic.


#import <BloodMagic/Final.h>

@interface FinalizedObject : NSObject
    <BMFinal>

@property (nonatomic, strong, bm_final) NSString *almostImmutableProperty;

@end

@implementation FinalizedObject

@dynamic almostImmutableProperty;

@end

// ...

FinalizedObject *object = [FinalizedObject new];
object.almostImmutableProperty = @"Initial value"; // everything is fine
object.almostImmutableProperty = @"Another value"; // exception will be thrown

Preferences

pod 'BloodMagic/Preference', :git => 'https://github.com/railsware/BloodMagic.git'

Enjoy the simplest way to deal with NSUserDefaults

#import <BloodMagic/Preference.h>

@interface Settings : NSObject
    <BMPreference>

@property (nonatomic, strong, bm_preference) NSString *nickname;

@end

@implementation Settings

@dynamic nickname;

@end

// ...

Settings *settings = [Settings new];
settings.nickname = @"AlexDenisov"; // @"AlexDenisov" goes to [NSUserDefaults standardUserDefaults] with key "nickname"
NSLog(@"My name is: %@", settings.nickname); // reads object for key "nickname" from [NSUserDefaults standardUserDefaults]

Side effects (aka bugs)

BloodMagic may have side effects, if you find one, please, open issue or send us a pull request.

Those actions will help us to protect you from mutilation.