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
Presentation by Ievgen Solodovnykov
pod 'BloodMagic', :git => 'https://github.com/railsware/BloodMagic.git'
$ 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
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
Preferences (NSUserDefaults wrapper)
BloodMagic has been designed to be extensible, so few more spells will be available soon.
====
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.
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 @protocol
s as well
BMInitializer *initializer = [BMInitializer injectableInitializer];
initializer.protocols = @[ @protocol(ProgressViewServiceProtocol) ];
initializer.initializer = ^id (id sender){
return [[ProgressViewService alloc] initWithViewController:sender];
};
[initializer registerInitializer];
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
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
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
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]
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.