LKUserDefaults 公開

2014年1月10日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

前回(Cocoaの日々: NSUserDefaults の値をプロパティアクセスできるようにする)のコードをライブラリ化して公開した。



使い方は、まずサブクラスを作りプロパティを書く。
#import "LKUserDefaults.h"

@interface UserDefaults : LKUserDefaults
@property (strong, nonatomic) NSString* name;
@property (strong, nonatomic) NSString* email;
@end

後はプロパティへアクセスするだけで自動的に NSUserDefaultsへの読み書きが行われる。
UserDefaults* defaults = [UserDefaults sharedInstance];
defaults.name = @"Hoge";        // キー "name" で NSUserDefaults へ書き込み
    :
NSString* name = defaults.name; // キー "name" の値を NSUserDefaults から読み出し

初期値を設定しておくこともできる。
- (void)registerDefaults
{
    self.name = @"unkown";
    self.name = @"unkown@xcatsan.com";
}

上記を定義しておくと、未設定の値を読みだした時にこの値が使われる。

定義可能なプロパティの型は NSUserDefaults と同じ。
@property (assign, nonatomic) BOOL boolValue;
@property (assign, nonatomic) NSInteger integerValue;
@property (assign, nonatomic) float floatValue;
@property (assign, nonatomic) double doubleValue;

@property (strong, nonatomic) NSString* stringValue;
@property (strong, nonatomic) NSArray* arrayValue;
@property (strong, nonatomic) NSArray* stringArrayValue;
@property (strong, nonatomic) NSDictionary* dictionaryValue;
@property (strong, nonatomic) NSData* dataValue;
@property (strong, nonatomic) NSURL* urlValue;
@property (strong, nonatomic) NSDate* dateValue;


オブジェクト型のプロパティの定義では ARC属性を strongにしておく。これは上記 registerDefaults の値を保持しておくため。

- - - -
実装のポイントは前回解説のように NSProxy をかましてプロパティへのアクセスをフックしているところにある。

前回記事:Cocoaの日々: NSUserDefaults の値をプロパティアクセスできるようにする

ライブラリでは汎用性のある部分を抽出して LKKeyValuStore と LKPropertyHook というクラスを作り、それを LKUserDefaults と LKUserDefaultsProxy が継承するようにしている。この為、若干読みづらいコードになっているかもしれない。今回は用意していないが NSUbiquitosKeyValueStore 用のクラスを用意すればiCloudの値も同じように使うことができる(たぶん)。


ハマったところとしては NSURL の格納部分。NSURL自体は NSuserDefaults のファイル形式である Property List には入れられない型だが NSUserDefaults では専用のメソッド(setURL:forKey:, URLforKey:)を用意していて、内部的に NSDataに変換して Property List へ入れている。この為、プロパティアクセスをフックしている今回の場合はプロパティの型情報を見てNSURLの時だけ適宜 NSDataへ変換してやる必要がある。この型判定がくせものでランタイム時に提供される情報(NSMethoSignatureなど)では引数/戻り値のクラスを知る方法を見つけることができなかった(オブジェクト=idであることしかわからなかった)。setterの場合は引数の型でそれが判断できるが getterの場合 Property List から取り出した NSData から判断するしかない。この為今回は NSUserDefaults から取得したオブジェクトが NSData の場合は無理やり URLforKey: を使って変換を掛けて、変換に成功したら NSURL として返し、失敗したら例外をキャッチして NSData のまま返すようにしている。ちょっとむりくりだけど。

なお形態としてはシングルトンになるが、複数クラスを定義したり、それらを継承関係にしたりすることができるようにしてある。ただしアクセス対象の NSuserDefaults は一つだけ(standardUserDefaults)。


課題・エンハンス案など
・現在はプロパティの先頭が小文字で始まることを前提にしている
・プロパティ名を途中で変更した場合、旧名の値が残り続ける(もしくは引き継げない)
・クラス定義毎に NSuserDefaults を作ることができるようにする
・擬似的な名前空間を作れるようにする(例えば VolumeSetting.name のようなキーで登録できるようにする)

何かあれば pull request をどうぞ。



NSUserDefaults の値をプロパティアクセスできるようにする

2014年1月8日水曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

Defaults値を保存したい。あるいは参照したい。ベタには必要な箇所で書く(あちこち)。
[[NSUserDefaults.standardUserDefaults setObject:@"hoge" forKey:@"name"];
でもキー名を直接書くわけにもいかず定数定義のヘッダを作る。
[[NSUserDefaults.standardUserDefaults setObject:@"hoge" forKey:KEY_NAME];
でも synchronize を忘れたりするのでいっそのこと独自クラスを作ってそちらで管理する。
[myDefaults setObject:@"hoge" forKey:KEY_NAME];
でも定数定義が毎回面倒。
なによりも設定/参照のコードが長い。なんかかえって手間が増えてるような。。もちろん一括管理で後でのメンテはしやすい。でもなんかちがう感が。

本当はプロパティでアクセスできるのがベスト。こんなイメージ。
myDefaults.name = @"hoge"
こうすると自動的に NSUserDefaults へ書き込んでくれる。
これなら定数定義も不要だし、コード補完にコンパイルチェックもできる。何よりも書いていて気分いい。



そんなわけで今回はこんなオレオレデフォルトを作ってみる。

完成イメージはこう
@interface UserDefaults : NSObject

@property (weak, nonatomic) NSString* string;
@property (assign, nonatomic) BOOL flag;
@property (assign, nonatomic) NSInteger integer;

+ (instancetype)sharedDefaults;

@end
プロパティの string や flagへ代入すると NSUserDefaults へ書き込み、逆に参照すると NSUserDefaults から読みだして返す。格納時のキー名はプロパティ名となる(例えば @"name")。


これを実現するにはプロパティへの書き込み・読み出しをフックする必要がある。プロパティへの書き込みをフックできれば、そのタイミングで渡された値をメンバ変数へ格納する代わりに NSUserDefaults へ書き出すことができる。

幸い Objective-C/Cococa はメソッドのフォワーディング機構があり、それを利用することでこれを実現できる。フォワーディングの利用アイディアは下記がとても参考になった。

NSProxy を使って UIWebView のイベントハンドリングをフックする

上記からコードを少々拝借して UserDefaultsProxy という NSProxy のサブクラスを作る。
@interface UserDefaultsProxy : NSProxy
@property (strong, nonatomic) NSObject* target;
- (instancetype)initWithTarget:(NSObject *)target;
@end

@implementation UserDefaultsProxy

- (instancetype)initWithTarget:(NSObject *)target
{
    self.target = target;
    return target ? self : nil;
}

UserDefaults.sharedDefaults ではこのプロキシのインスタンスを返すようにする。
@implementation UserDefaults
//@dynamic name;    // not work

+ (instancetype)sharedDefaults
{
    static UserDefaults* _sharedDefaults = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedDefaults = (UserDefaults*)[[UserDefaultsProxy alloc] initWithTarget:UserDefaults.new];
    });
    return _sharedDefaults;
}@end
こうすると UserDefaults へのメッセージはすべて UserDefaultsProxy へ送られるようになる(メッセージ転送)。つまりプロパティアクセスは実際には UserDefaultsProxy が処理することになる。

UserDefaultsProxy は UserDefaults で実装されているプロパティ(実体はsetter/getterメソッド)を実装していないので普通はこのままだとランタイムエラーになるが、NSProxy(NSObject)で用意されている forwardInvocation: を実装しておくと、未実装のメソッド呼び出しはすべてここでハンドリングすることができるようになる。つまりここで NSUserDefaults へ書き込み・読み出ししてやればいいわけだ。

(イメージ)
UserDefaults.sharedUserDefaults.name = @"hoge";
↓
↓ メッセージ(セレクタ) setName:
↓
-[UserDefaultsProxy forwardInvocation:]

実際には forwardInvocation: と合わせて methodSignatureForSelector: も実装してセレクタ→シグネチャ変換の定義をしておく必要もある。シグネチャ(引数、戻り値の型情報)は元のクラス(UserDefaults)が持っているので取っておいた self.target のものを返してやればいい。
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel
{
    return self.target ? [self.target methodSignatureForSelector:sel] : [super methodSignatureForSelector:sel];
}


さていよいよ forwardInvocation: の実装
- (void)forwardInvocation:(NSInvocation *)invocation
{
    if (self.target) {
// [1]
        invocation.target = self.target;
        // not call [invocation invoke];

// [2]
       NSString* selectorName = NSStringFromSelector(invocation.selector);
        NSString* key = nil;

// [3]
        if (invocation.methodSignature.numberOfArguments > 2) {

// [4]
            // setter
            // "set" +  + ":"
            key = [selectorName substringWithRange:NSMakeRange(3, selectorName.length-(3+1))].lowercaseString;
            id value = [self _getArgumentAtIndex:2 invocation:invocation];
            [NSUserDefaults.standardUserDefaults setObject:value forKey:key];
            [NSUserDefaults.standardUserDefaults synchronize];

        } else {
// [5]
            // getter
            key = selectorName;
            id ret = [NSUserDefaults.standardUserDefaults objectForKey:key];
            [self _setReturnValue:ret invocation:invocation];
        }

    }
}
[1] 前準備。通常は invoke を実行して self.targetの実際のメソッドを呼び出す。今回 self.target(つまりUserDefaults)のプロパティの実体は NSUserDefaults にするのでメンバ変数への書き込みは必要ないので呼び出さない。@propertyによって暗黙的にメンバ変数の定義されるのだがこれは使わない。

[2] 後で使う情報を用意。key は NSUserDefaults への格納キー名として使い、プロパティ名を割り当てる。

[3] setter/getterの分岐。メソッドの引数の1、2番めはランタイムが使うので3番め以降の存在で判断する。例えば setName: だと引数=3、nameだと引数=2。

[4] setterの場合は先頭の "set" と最後の ":" を取り除き、さらに小文字にしたものがキー名となる。"setName:" なら key=@"name" となる。 次に渡された引数を取り出す。ここは引数の型によって細かく処理する必要があるので別メソッドで処理している。

こんな感じ(長いので割愛)。
- (id)_getArgumentAtIndex:(NSInteger)index invocation:(NSInvocation*)invocation
{
    const char* valueType = [invocation.methodSignature getArgumentTypeAtIndex:index];
    while(strchr("rnNoORV", valueType[0]) != NULL) {
  valueType += 1;
    }

    switch (valueType[0]) {
        case 'c':
        {
            char value;
            [invocation getArgument:&value atIndex:index];
            return [NSNumber numberWithChar:value];
        }
        case 'i':
        {
            int value;
            [invocation getArgument:&value atIndex:index];
            return [NSNumber numberWithInt:value];
        }
   :
扱い易いように整数とかのプリミティブな型は全て NSNumber につめて取り出す。
この辺りは下記コードが参考になった。
NSInvocation+OCMAdditions.m

あとは NSUserDefaults へ詰めるだけ。

なおオブジェクト型を取り出す時は __unsafe_unretained を忘れずに(ARC環境で)。当初これをやらなくて数秒後にクラッシュするという謎の問題が起きてハマった。
case '#':
        case '@':
        {
            __unsafe_unretained id value;
            [invocation getArgument:&value atIndex:index];
            return value;
        }
NSUserDefaults は非同期で書きだすのでそのタイミングで参照した時に寿命が尽きていたということらしい。

[5] getterの実装。こちらも戻り値の型にごとの処理が必要なので別メソッド処理。
- (void)_setReturnValue:(id)value invocation:(NSInvocation*)invocation
{
    const char* valueType = [invocation.methodSignature methodReturnType];
    while(strchr("rnNoORV", valueType[0]) != NULL) {
  valueType += 1;
    }
    
    switch (valueType[0]) {
        case 'c':
        {
            char ret = [value isKindOfClass:NSNumber.class] ? ((NSNumber*)value).charValue : 0;
            [invocation setReturnValue:&ret];
            break;
        }
        case 'i':
        {
            int ret = [value isKindOfClass:NSNumber.class] ? ((NSNumber*)value).intValue : 0;
            [invocation setReturnValue:&ret];
            break;
        }
   :
さっきとは逆で取り出した NSNumber を型毎に変換して返してやる。戻り値は -[NSInvocation setReturnValue:(void*)ret] にセットしておけば呼び出し元に返るようになっている。


さて使ってみよう。
NSLog(@"string=%@", UserDefaults.sharedDefaults.string);
UserDefaults.sharedDefaults.string = [NSString stringWithFormat:@"%@", NSDate.date];
    
NSLog(@"flag=%d", UserDefaults.sharedDefaults.flag);
UserDefaults.sharedDefaults.flag = YES;

NSLog(@"integer=%d", UserDefaults.sharedDefaults.integer);
UserDefaults.sharedDefaults.integer = 1000;

結果
ProxySample[45342:70b] string=2014-01-07 14:37:50 +0000
ProxySample[45342:70b] flag=1
ProxySample[45342:70b] integer=1000
動いた。わかりずらいが、初回は値なしで表示され次回以降は NSUserDefaults から読み出された値が表示されるようになる。

forwardInvocation: の実装を変えれば NSUserDefaults 以外にも使える。例えばキャッシュとか iCloud へ保存するとか。あとキー名をらくだ文字(MacBookAirとか)にしたい時はもうひと手間必要。

なお今回 NSProxy を使ったが実際は NSObject でも動く。違いは何かと言われると今回の用途ではおおざっぱなところでは無い気がする。NSProxyは元々こういう目的で使われるのでラベリング的な目的が大きいか(実装が少ないというのもあるし、isProxyメソッドで区別ができるとかそんなところ?)。

ともあれようやく面倒な定数定義から開放されそう。


サンプルコードはこちら
iOS7-Sample/ProxySample
※分類上 iOS7になっているが今回のコードは iOS7固有では無い

問題や改良があれば是非(pullrequestとかも可)。


参考情報:
ダイナミックObjective-C / 80 デザインパターンをObjective-Cで - Proxy (2)
ダイナミックObjective-C / 45 AspectCocoa (3) - フォワーディングとポージングの利用
Obj-C関係で木下氏の記事は鉄板。

Objective-C Runtime Programming Guide - Type Encodings
NSMethodSignature から得られる型のエンコード一覧表。



- - - -
Objective-C が面白いと感じるのはこういうところだなー。今回のは車輪の再発明だと思うけど書いていて面白かった。

Autolayoutでビューを等間隔に並べる

2014年1月7日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

こんなやつを作りたい。

Autolayoutで等間隔にビューを並べる方法はガイドに記載がある。
Auto Layout ガイド - ビューを等間隔で並べる (P.31)

ビューの等間隔配置の機能は Autolayoutの自体には無くて、それぞれのビューの間にスペーサとなるビューを入れて間隔を調整する方法を取るらしい。

なるほど。スペーサ幅を可変にして、かつ全スペーサの幅が同じになるような制約が作れればいいわけだ。

実際やってみよう。
こんな感じでビューを配置して制約を作る。
スペーサ(黄色とオレンジ色のやつ)はデバッグしやすいように色をつけて縦位置も意図的にずらしてある。
制約を作るにはコントロールキーを押しながらスペーサをクリックして、そのままドラッグしてビュー(緑色)へ接続する。
すると作成可能な制約の一覧が出てくるので Horizontal Spacing を選ぶ。
これだけ。
この操作を全てのスペーサについて行う。

また全てのスペーサが同じ幅(width)を持つように設定しておく。これは上記のようなドラッグ操作を最初のスペーサと他のスペーサ間で行い Equal Widths を選べばいい(最初のスペーサを基準にしておく)。

その他、縦位置はお好みで。例ではビューが画面縦の中央に配置するようにしてある。


これで終わり。コーディングは無し。

実行してみよう。

iPhone縦

iPhone横

iPad縦

iPad横


いい感じ。
やってみた感じではドキュメントにあるような Relation=Less than or Equal や Priority=999の設定は不要だった(むしろ設定するとうまく行かなかった。やり方がまずかったのかも)。

サンプルのソース
iOS7-Sample/AutolayoutSample2

- - - -
Autolayoutは強力だがプリミティブすぎて少しでも凝ったことをやろうとすると今のままではちょっと面倒だし難解。今後は等間隔配置のような1段上のレベルのレイアウトを支援するような機能が望まれる。例えばInterfaceBuilder上でビューを複数選んでボタンをポチると配置が完了するとか、等間隔を管理するクラスを導入するとか(これはもしかするとサードパーティから出てるかもしれない)。




Autolayoutで可変高さのビューをアニメーション

2014年1月5日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

制約の追加・削除でアニメーションを制御するTips

例えば下図のような緑色のビュー(以下、緑ビュー)があるとする。

最初は赤いビュー(以下、赤ビュー)は隠れていてOpenボタンを押すと下から上へせり出てくるようなアニメーションをやってみる。



固定高さの場合は簡単で赤ビューの上の制約のconstant値を変えてやればいい。ただ今回のように出現位置(緑ビューの上辺)が可変の場合は少し工夫がいる。試行錯誤してみたところあらかじめ2つの制約(上辺と高さ)を用意しておき、それを操作することで実現できることがわかった。

順を追ってみてみる。

1. 赤ビューの制約を設定
まず赤ビューを緑ビューの上へ配置する。

次に制約を追加する。上下左右の制約をconstatn=0とした上で同時に Height=0とする。上下制約で高さを可変にしつつ、隠す時には高さを0にしておくという意図でこうする。
ただXcodeは警告を出す。

frame.size.heightが非0なのに(目で見て分かるように赤ビューは高さがある)制約height=0にしているので制約とframe設定が不一致という警告。ここでframe.height=0とすると今度は上制約(constatn=0)と矛盾して別の警告が出る。これは2つの矛盾する制約を課しているからこれは当たり前。
制約1:上辺は superviewとのオフセット0(プライオリティ 1,000)
制約2:高さは0(プライオリティ 1,000)
フレーム:高さは(ここでは282)

矛盾する制約がある場合はプライオリティに差をつけてやれば良い。今回は高さの制約のプライオリティを下げてやる。ここでは 500にしてみた。

すると警告が消えて代わりに高さの制約が点線表示になる。制約1の方が優先されるので赤ビューは見えたまま。

2. 制約の制御
次に赤ビューをアニメーションさせるコードを書く。
まず赤ビューの上の制約のアウトレット topConstraint を作っておく。

後での操作の為に ARCはweakではなくstrongにしておく。

初期表示時には赤ビューは隠しておきたい。そこで初期処理で制約1(上辺の制約)を削除してしまう。
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.view removeConstraint:self.topConstraint];
}
こうすると制約2(高さ0)の制約が残るので最初は何も表示されない。

次に赤ビューを表示させる。openボタンのアクションで取っておいた上の制約を加えてやる。
- (IBAction)open:(id)sender {
    [self.view addConstraint:self.topConstraint];
    [UIView animateWithDuration:0.5
                     animations:^{
                         [self.view layoutIfNeeded];
                     }];
}
こうすると制約1(上辺の制約)が復活し、かつ制約2(高さ0)よりもプライオリティが高いので優先され、画面上部まで上辺が移動する。
赤ビューが上へせり出してきて
アニメーション完了。制約1(上辺の制約=0)が適用された状態で終わる。

なおアニメーションの layoutIfNeededは制約ベースのアニメーションの定石(これをやらないとアニメーションしない)。

逆に閉じたい時は制約1(上辺の制約)を削除してやればいい。
- (IBAction)close:(id)sender {
    [self.view removeConstraint:self.topConstraint];
    [UIView animateWithDuration:0.5
                     animations:^{
                         [self.view layoutIfNeeded];
                     }];
}

Autolayoutを活用しているのでもちろん4インチディスプレイでも意図通りに動く。


なおアウトレットを作る代わりにコードで制約のインスタンスを作る方法もある。ただ今回のようにInterfaceBuilderを使った方が見た目わかりやすいのと制約をIB側でまとめられるメリットがあると思う。

- - - -
Autolayout以前のアニメーションは frameに対する操作が基本だったが、
Autolayoutでアニメーションを行う時の基本は
『制約に対して操作する』
これを理解すると難しくない(...いや凝ったことは難しいが)。ちょっとしたパズルみたい。

今回の方法がベストかは良くわからないがうまくいった。Autolayoutはとっつきにくいが慣れてくるとかなり強力。


サンプルのソースコードはこちら
AutolayoutSample1


[100min]

あけましておめでとうございます 2014

2014年1月1日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加


2014年もよろしくお願いいたします。
写真は鹿島灘海浜公園での初日の出。いい日の出が見られました。寒かったけど。

昨年は新作アプリを4本リリースできました。


どれも鳴かず飛ばずで寂しい限りですがそれでも「行動への自信」と「最新OSのキャッチアップ」ができたのが収穫だったと思います。今年はこれを教訓に「悩むよりも手を動かせ」をモットーに進みたいと思います。

昨年の年初目標では AndroidアプリとWin8アプリとか寝言を書いていましたが、本業ならともかく仕事以外・子育て・家族サービス以外のスキマ時間でiOS以外のアプリ開発を行うには私の能力では到底無理と改めて実感しました。その代わり特に後半はブログを書く暇も惜しんでひたすらiOSアプリを作っていました。そのかいもあって手の遅い私でも昨年後半には1〜2ヶ月に1本のアプリをリリースすることができました(12月は1つのアプリの5回くらいのアップデートも実施)。これには自分でも意外で妙な自信がつきました。売れなかったけど。

そんな自信も出てきたので、今年は調子に乗って無謀にも10本の新作を目標にすることにしました。また今年はゲーム(カジュアルかパズルのようなやつ)も作ってみようと目論んでます。

2014年目標
・10本の新作アプリリリース
・ゲームアプリのリリース

ブログはその分停滞しほとんど更新できないと思います。
ただ情報局の半自動化は考えていてこれはRailsを勉強しながら少しづつ進めて行こうと考えています(時間かかるけど)。

今年もよろしくお願いします。


鋭意開発中↓


プロトタイプアプリを並行していろいろ




[iOS7] UITableView の境界線を separatorInset= UIEdgeInsetsZero で変えた時の副作用

2013年12月9日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

iOS7になってから UITableView の区切り線の左が短く切れた表示がデフォルトとなった。

iOS6までのように左まで線を引くにはいくつか方法があって一番簡単なのは appearanceを使う方法。ネットでたくさん出てた。
[UITableViewCell appearance].separatorInset = UIEdgeInsetsZero;
するとこうなる。アプリで使うすべての UITableViewが対象になるのでこれは便利。


ところがこれを使った場合メール送信のモーダルビュー表示で問題が出た。ツールバー上のボタン押下で MFMailComposeViewControllerを表示していたところ、iOS7だとメール編集画面が一瞬表示されてしまった後にすぐに閉じてしまうという現象が出た。

↑一瞬出てすぐ消える。
コンソールには次のエラーが出てた。
_serviceViewControllerReady:error: Error Domain=NSCocoaErrorDomain Code=4097
 "The operation couldn’t be completed. (Cocoa error 4097.)"
なんかおかしい。同じプログラムでも iOS6で動かすとこの現象は出ない。

調べてみると UIBarButtonItem などのアピアランスをカスタマイズしているとこの現象が出るらしい。

Cannot show modal ViewController in iOS7

MFMailComposer Dismissing immediately after launch


ただ自分のところでは UIBarButtonItem.appearance は設定していない。ネットで色々調べたが最初は原因がわからなかった。 コードを見なおしていると appearanceを設定している箇所 [UITableViewCell appearance].separatorInset にふと気が付き、試しにこれをコメントアウトしたところ正常動作するようになった。これが原因だったか。

これでモーダルビューの表示は解決したが、このままだと境界線が切れたままになるので境界線の設定を別の方法を使う必要がある。調べたら下記サイトが参考になった。

AmebaアプリのiOS7対応時に行ったUI実装

appearanceで一律変更設定をするのではなく Interface Builder でテーブル一つ一つの Separator Insets を設定してやる。
タイプを Defaults → Custom に変更しLeft=15を Left=0にしてやる。
テーブルがたくさんある時はコードで対処するのが良さそう。自分の場合は全部手で設定した。




最速カレンダー登録アプリ KickCal をリリースしました

2013年11月12日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

お手軽にデータ登録できる Kickシリーズ(?)の第2弾 KickCal をリリースしました。iOS7専用です。





取り立てて新しいものではありませんが個人的に欲しかったので作りました。単一機能に絞り、シンプルで使い勝手の良いアプリを目指しました。小さなアプリなので開発面では Autolayout や Storyboardを実戦で使う良い機会になりました。


半透明効果には下記のライブラリを使っています。iOS7っぽいところを出したいと思い、多少デザインの統一感が崩れますが入れてみました。


Kickシリーズはもう一本作ろうと思ってます。
無料なので良ければお試し下さい。



人気の投稿(過去 30日間)