Reactive-Evernote-SDK-iOSを作りました

Reactive-Evernote-SDK-iOSを作りました。Evernote SDKをReactiveCocoaで使えるライブラリーです。GitHubで公開しています。

下記のように使います。

[[[RACEvernoteNoteStore noteStore] listNotebooks] subscribeNext:^(NSArray *notebooks) {
    NSLog(@"%@", notebooks);
}                                                         error:^(NSError *error) {
    NSLog(@"error: %@", error);
}                                                     completed:^{
    NSLog(@"completed");
}];

さすがにThriftのコードジェネレータを作成するのは難しいので、[RACSignal createSignal:]で既存のEDAM系のクラスを呼んでいます。

まだlistNotebookとlistTagsしか試してないので、お暇な方は試してバグ報告よろしくです。podspecもあるのでCocoaPodsで簡単に導入できると思います。

Objective-CのFunctional Reactive ProgrammingフレームワークReactiveCocoaを試してみる

今、一部で話題のReactive Programmingですが、Objective-CにもReactiveCocoaというフレームワークが存在します。ということでReactiveCocoaを試しに使ってみます。

GitHubにソースコードを置きました。

iOSアプリのシングルビュー構成のプロジェクトを作成します。

今回はReactiveCocoaはCocoaPodsでインストールします。Podfileを作成してpod ‘ReactiveCocoa’と記述します。

まず、テキストフィールドへの入力によってボタンの表示・非表示を行ってみます。
StoryboardでビューにUITextFieldを二つとUIButtonを一つ配置し、コントローラのヘッダにプロパティを作成します。

@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
@property (weak, nonatomic) IBOutlet UIButton *logInButton;

コントローラのviewDidLoadで以下のように記述します。

    [self.usernameTextField.rac_textSignal subscribeNext:^(NSString *text) {
        self.logInButton.hidden = (text.length == 0);
    }];

UITextFieldのカテゴリメソッドrac_textSignalでtextの変更のシグナルのRACSignalが取得できます。RACSignalとはその名の通りイベントのシグナルです。
シグナルが正常に発生するとsubscribeNext:で登録したブロックが実行されます。ログインボタンのhiddenにテキスト長が0かの真偽値を設定しています。これでusernameのtextが入力されていない場合にlogInButtonが非表示になり、値を入力するとlogInButtonが表示されます。

今度はusernameとpasswordに入力するとログインボタンを表示するように変更してみます。先ほどのコードを削除して以下のコードを記述します。

    [[RACSignal combineLatest:@[self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal] reduce:^(NSString *username, NSString *password) {
        return @(username.length == 0 || password.length == 0);
    }] subscribeNext:^(NSNumber *hidden) {
        self.logInButton.hidden = hidden.boolValue;
    }];

二つのUITextFieldのシグナルを[RACSignal combineLatest:reduce:]でまとめて、一つのシグナルとしてあつかいます。combineLatestで二つのシグナルを登録して、reduceで二つのテキストの長さのいずれかが0の場合にYES(のNSNumber)を返しています。
これでusernameとpasswordのtextが入力されていない場合にlogInButtonが非表示になり、どちらともに値を入力するとlogInButtonが表示されます。

次に非同期処理としてTwitterからユーザ名を取得してみます。
Storyboardにボタンとラベルを追加して、コントローラのヘッダのプロパティにバインドします。

@property (weak, nonatomic) IBOutlet UIButton *twitterButton;
@property (weak, nonatomic) IBOutlet UILabel *twitterLabel;

viewDidLoadには以下のように記述します。

    @weakify(self);
    [[self.twitterButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *button) {
        [[[[TwitterManager sharedManager] requestAccessToAccounts] sequenceNext:^{
            return [[TwitterManager sharedManager] fetchName];
        }] subscribeNext:^(NSString *name) {
            @strongify(self);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.twitterLabel.text = name;
            });
        }          error:^(NSError *error) {
            @strongify(self);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.twitterLabel.text = error.localizedDescription;
            });
        }];
    }];

rac_signalForControlEvents:でボタンのイベントのシグナルを取得して、subscribeNext:でボタンのイベントで実行するブロックを登録しています。
TwitterManagerのrequestAccessToAccountsを実行すると、これもシグナルを返します。それが成功すると、fetchNameを実行して、さらにシグナルを返しています。そのシグナルが成功するとtwitterLabel.textに名前を設定し、エラーが発生するとエラー内容を設定しています。

TwitterManagerでの非同期処理の記述の仕方は以下のようになります。

+ (TwitterManager *)sharedManager {
    static TwitterManager *_instance = nil;
 
    @synchronized (self) {
        if (_instance == nil) {
            _instance = [[self alloc] init];
        }
    }
 
    return _instance;
}
 
- (id)init {
    self = [super init];
    if (self) {
        self.accountStore = [[ACAccountStore alloc] init];
        self.accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
    }
    return self;
}
 
- (RACSignal *)requestAccessToAccounts {
    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
        [self.accountStore requestAccessToAccountsWithType:self.accountType options:nil completion:^(BOOL granted, NSError *error) {
            if (granted) {
                NSArray *accounts = [self.accountStore accountsWithAccountType:self.accountType];
                if (accounts.count > 0) {
                    self.account = accounts[0];
                    [subscriber sendNext:nil];
                } else {
                    [subscriber sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil]];
                }
            } else {
                [subscriber sendError:(error ? error : [NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil])];
            }
 
            [subscriber sendCompleted];
        }];
 
        return [RACDisposable disposableWithBlock:nil];
    }];
 
    return signal;
}
 
- (RACSignal *)fetchName {
    @weakify(self);
    RACSignal *signal = [RACSignal startWithScheduler:[RACScheduler scheduler] subjectBlock:^(RACSubject *subject) {
        @strongify(self);
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://api.twitter.com/1.1/users/show.json?screen_name=%@", self.account.username]];
        SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:url parameters:nil];
        request.account = self.account;
        [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
            if (error) {
                [subject sendError:error];
            } else {
                NSError *subError;
                NSDictionary *status = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&subError];
                if (subError) {
                    [subject sendError:subError];
                } else if (status[@"name"]) {
                    [subject sendNext:status[@"name"]];
                } else {
                    [subject sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:2 userInfo:nil]];
                }
            }
 
            [subject sendCompleted];
        }];
    }];
 
    return signal;
}

requestAccessToAccountsは[RACSignal createSignal:]でシグナルを作成して返しています。リクエストが成功すると[subscriber sendNext:status[@”name”]]でシグナルを送り、エラーの場合はsendError:でエラーを送ります。完了するとsendCompletedを送ります。RACSignalではsubscribeNext:やsubscribeError:以外にもsubscribeCompletedで完了通知を受けとることができます。return [RACDisposable disposableWithBlock:];でキャンセルする時の処理を記述して返します。今回はキャンセル処理は無いのでnilを渡しています。(createSignal:って非同期処理に利用していいのかな?このあたり良く理解していないです。)

fetchNameでは[RACSignal startWithScheduler:subjectBlock:]でシグナルを作成して返しています。RACDisposableを返さない事以外はcreateSignal:とほぼ同様のようです。

ということで、ReactiveCocoaを試してみましたが、非Reactive Programmingとはかなり記述が変わってきますね。ReativeCocoaを使いこなせばUIイベントの処理や非同期処理が記述しやすくなる感触が得られました。

UIViewのローカライズ作業をMethod Swizzlingで簡単にする

iOSでStoryboardやXIBのUILabelやUIButtonなどのローカライズを行うことに関してEZ-NET: UIView のローカライズ作業を簡単にするという記事がありました。その記事ではサブクラス化してローカライズを行っているのですが、サブクラスでなくてカテゴリーで実装することもできるんじゃないかと思ってやってみました。

まずMethod Swizzlingを行うクラスを作成します。

#import <Foundation/Foundation.h>
 
@interface MethodSwizzling : NSObject
+ (void)swizzleMethod:(Class)aClass from:(SEL)originalSelector to:(SEL)newSelector;
@end
 
#import <objc/runtime.h>
#import "MethodSwizzling.h"
 
@implementation MethodSwizzling {}
+ (void)swizzleMethod:(Class)aClass from:(SEL)originalSelector to:(SEL)newSelector {
    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method newMethod = class_getInstanceMethod(aClass, newSelector);
 
    if (class_addMethod(aClass, originalSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
        class_replaceMethod(aClass, newSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, newMethod);
    }
}
@end

そしてUILabelやUIButtonのカテゴリーを作成します。

@interface UILabel (Localize)
- (void)localizedAwakeFromNib;
@end
 
@implementation UILabel (Localize)
+ (void)load {
    [MethodSwizzling swizzleMethod:[UILabel class] from:@selector(awakeFromNib) to:@selector(localizedAwakeFromNib)];
}
 
- (void)localizedAwakeFromNib {
    self.text = NSLocalizedString(self.text, nil);
 
    [self localizedAwakeFromNib];
}
@end
 
@interface UIButton (Localize)
- (void)localizedAwakeFromNib;
@end
 
@implementation UIButton (Localize)
+ (void)load {
    [MethodSwizzling swizzleMethod:[UIButton class] from:@selector(awakeFromNib) to:@selector(localizedAwakeFromNib)];
}
 
- (void)localizedAwakeFromNib {
    [self setTitle:NSLocalizedString(self.titleLabel.text, nil) forState:UIControlStateNormal];
 
    [self localizedAwakeFromNib];
}
@end

Method SwizzlingでawakeFromNibの代わりにlocalizedAwakeFromNibが実行されるように変更しました。
これでUILabelやUIButtonに設定した文字列がローカライズされて表示されます。

問題が出るかもしれないけど、一応こういう方法でも可能だということで。

Evernoteにファイルを保存するフォルダアクションの作り方

指定したフォルダに入れたファイルをEvernoteにクリップするEverDesktopというアプリが公開されました。850円なので躊躇される方もいるかと思うので、無料で似たような機能を作成する方法を解説します。

まず機能を作成する前に確認。
ファイルをクリップするだけならDockのEvernoteアイコンにファイルをドラッグ&ドロップするだけで可能です。
Evernote Icon

さて、フォルダにファイルを入れてクリップするためにはフォルダアクションを利用します。フォルダアクションというのは、指定のフォルダにファイルやフォルダが追加されたら動作する機能です。

フォルダアクションを簡単に作るにはAutomatorを利用します。

Automatorを起動します。起動したらフォルダアクションを選択します。

Voila_Capture7.png

フォルダアクションを作成するための画面が表示されます。フォルダアクションを付けるフォルダを選択します。

Voila_Capture8.png

AppleScriptを利用して機能を作成するので、「AppleScript を実行」というアクションをドラッグして追加します。
Voila_Capture9.png

追加すると以下のようになります。

Voila_Capture10.png

そこへ以下のAppleScriptスクリプトをペーストします。

on run {input, parameters}
	tell application "Evernote"
		repeat with x in input
			try
				create note from file x
			on error error_message number error_number
				display alert "Send to Evernote Failed" message "Error:     " & error_message & "
" & "Error Number:  " & error_number as warning
			end try
		end repeat
	end tell
	return input
end run

※このスクリプトはAutomator.appを使って、Evernoteにファイルを転送する – fav Apple!から流用させてもらいました。ありがとうございました。

Voila_Capture11.png

名前を付けて保存すると完成です。
クリップしたいファイルを最初に指定したフォルダに入れるとEvernoteにクリップされます。

また、以下のスクリプトを利用すると複数のファイルを一緒にフォルダに入れた時に1つのノートにクリップされます。

on run {input, parameters}
	tell application "Evernote"
		try
			create note title "Mixed note" with text "" attachments input
		on error error_message number error_number
			display alert "Send to Evernote Failed" message "Error:     " & error_message & "
" & "Error Number:  " & error_number as warning
		end try
	end tell
	return input
end run

EverDesktopにはクリップしたファイルを日付ごとに仕分けする機能がありますが、このAppleScriptスクリプトをカスタマイズすることで同様の機能を作成することも可能です。挑戦してみてください。

Rails3でHamlを利用する

Gemfileに

gem "haml"
gem "haml-rails"

を追加。

$ bundle

を実行。

erbをHamlに変換するためにerb2hamlを導入する。
Gemfileに

group :development do
  gem 'erb2haml'
end

を追加。

$ bundle
$ rake haml:convert_erbs

を実行。

PlayをHomebrewでインストール

$ brew upgrade
$ play new playapp                                                              
 
What is the application name? 
> Play App
 
Which template do you want to use for this new application? 
 
  1 - Create a simple Scala application
  2 - Create a simple Java application
  3 - Create an empty project
 
> 1
 
 
$ cd playapp
$ play
 
[Play App] $ run

http://localhost:9000にアクセスすると画面が表示された。

PaintCodeでiOSアプリ用ボタンを作ってみた

PaintCodeを買ってみたので、早速iOSアプリ用ボタンを作ってみました。

直感的に使用できるし、ボタン作成には丁度いいアプリでした。PDFで書き出すとAdobe Illustratorでレイヤーが保存された状態で利用できるので、効果をつけたりしたい場合はイラレに持っていくこともできます(それなら最初からイラレで作るって?)。やはりコードが出力されるのが魅力かな。

起動するとWindowが表示されます。
120319 0003

カンバスサイズを90×40に変更します。OS X/iOSのボタンをiOSに変更します。コードがiOS向けになります。原点表示も左上になります。
120319 0005

角丸長方形を描画。ストローク無しです。
120319 0006

#222222/#444444/#666666の色設定を追加。Hex Color Pickerで指定しました。
120319 0007

グラーションを追加。一番左が黒、左から2番目が#222222、3番目が#444444、一番右が#666666。Fill Styleに作成したグラデーションを指定。
120319 0009

Inner Shadowを作成して指定。
120319 0011

文字列を追加。
120319 0012

テキストにOuter Shadowを指定。
120319 0013

でき上がったコードがこれ。

//// General Declarations
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
 
//// Color Declarations
UIColor* color222222 = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 1];
UIColor* color444444 = [UIColor colorWithRed: 0.27 green: 0.27 blue: 0.27 alpha: 1];
UIColor* color666666 = [UIColor colorWithRed: 0.4 green: 0.4 blue: 0.4 alpha: 1];
 
//// Gradient Declarations
NSArray* gradientColors = [NSArray arrayWithObjects: 
    (id)[UIColor blackColor].CGColor, 
    (id)[UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 1].CGColor, 
    (id)color222222.CGColor, 
    (id)color444444.CGColor, 
    (id)[UIColor colorWithRed: 0.33 green: 0.33 blue: 0.33 alpha: 1].CGColor, 
    (id)color666666.CGColor, nil];
CGFloat gradientLocations[] = {0, 0.25, 0.45, 0.51, 0.75, 1};
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)gradientColors, gradientLocations);
 
//// Shadow Declarations
CGColorRef innerShadow = color444444.CGColor;
CGSize innerShadowOffset = CGSizeMake(0, -0);
CGFloat innerShadowBlurRadius = 2;
CGColorRef textShadow = [UIColor blackColor].CGColor;
CGSize textShadowOffset = CGSizeMake(0, -0);
CGFloat textShadowBlurRadius = 3;
 
//// Abstracted Graphic Attributes
NSString* textContent = @"Touch!";
UIFont* textFont = [UIFont fontWithName: @"Helvetica-Bold" size: 18];
 
 
//// Rounded Rectangle Drawing
UIBezierPath* roundedRectanglePath = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(0, 0, 90, 40) cornerRadius: 4];
CGContextSaveGState(context);
[roundedRectanglePath addClip];
CGContextDrawLinearGradient(context, gradient, CGPointMake(45, 40), CGPointMake(45, -0), 0);
CGContextRestoreGState(context);
 
////// Rounded Rectangle Inner Shadow
CGRect roundedRectangleBorderRect = CGRectInset([roundedRectanglePath bounds], -innerShadowBlurRadius, -innerShadowBlurRadius);
roundedRectangleBorderRect = CGRectOffset(roundedRectangleBorderRect, -innerShadowOffset.width, -innerShadowOffset.height);
roundedRectangleBorderRect = CGRectInset(CGRectUnion(roundedRectangleBorderRect, [roundedRectanglePath bounds]), -1, -1);
 
UIBezierPath* roundedRectangleNegativePath = [UIBezierPath bezierPathWithRect: roundedRectangleBorderRect];
[roundedRectangleNegativePath appendPath: roundedRectanglePath];
roundedRectangleNegativePath.usesEvenOddFillRule = YES;
 
CGContextSaveGState(context);
{
    CGFloat xOffset = innerShadowOffset.width + round(roundedRectangleBorderRect.size.width);
    CGFloat yOffset = innerShadowOffset.height;
    CGContextSetShadowWithColor(context,
        CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)),
        innerShadowBlurRadius,
        innerShadow);
 
    [roundedRectanglePath addClip];
    CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(roundedRectangleBorderRect.size.width), 0);
    [roundedRectangleNegativePath applyTransform: transform];
    [[UIColor grayColor] setFill];
    [roundedRectangleNegativePath fill];
}
CGContextRestoreGState(context);
 
 
 
 
//// Text Drawing
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, textShadowOffset, textShadowBlurRadius, textShadow);
CGRect textFrame = CGRectMake(5, 8, 80, 20);
[[UIColor whiteColor] setFill];
[textContent drawInRect: textFrame withFont: textFont lineBreakMode: UILineBreakModeWordWrap alignment: UITextAlignmentCenter];
CGContextRestoreGState(context);
 
 
//// Cleanup
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);

node.js/npm/expressのインストール with Homebrew

node.js/npm/expressをHomebrewを使いつつインストール。

$ brew install node.js
$ curl http://npmjs.org/install.sh | sh
$ npm install express
$ ln -s /usr/local/node_modules/express/bin/express /usr/local/bin/express