Cocoa練習帳 -13ページ目

[OSX][iOS] MOSA Software Meeting 2013 (MSM2013)

11/29(金)~11/30(土)に開催された MOSA Software Meeting 2013 (MSM2013) に参加してきた。

セミナーに参加する目的は人それぞれだろう。自分の場合は業界の動向を感じるとともに、自信のモチベーションを高めるという目的もあったが、今回は特に、本格的にアプリケーション開発を主業務にしてからの参加ということで、とても期待が高かったセミナーとなった。

MSM2013は、WWDCの様に同時に二つのセッションが開催される場合もあって、全てのセッションを視聴できないのが残念だが、自分は全二日間参加ということで、全てのセッションの資料を受け取れているので、後日の自主学習が可能ということで、助かっている。

アプリケーションに対しての要求は高まってきていて、使うことによって利用者に対して何かをもたらすアプリケーションがトレンドのようだ。それは、アプリケーションだけでなく、他のサービスや機器との連動がポイントとなる。なので、Bluetooth LEへの高い関心というのも、これが理由だろうか。また、UXについては、いやいや、自分でもやれることがあるのでは?と刺激を受けた。

XcodeやUIKitのセッションも、自主学習では知識にムラが出てしまっているところを網羅的に情報があられ、思わぬ収穫となった

テキスト関連のセッションは、他では知ることができない情報なので、いつか聞きたいと思っていたので、それが聞けて嬉しい。

Sprite Kitについては、とても刺激を受けた。よし、利用しよう。

ソースコード GitHubからどうぞ。
https://github.com/murakami/SimpleChart - GitHub
関連情報 MOSA Software Meeting 2013
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

関東第62回Cocoa勉強会

第62回のCocoa勉強会(関東)が開催された。内容は、以下のとおり。

 日時 2013/10/19(土) 13:00~18:00
 会場 水道橋 貸し会議室 内海 101会議室
 集合 現地
 会費 500円
 発表
  ・「新しい通信クラス群NSURLSessionを使ってみる」iOS
  ・「APIを前方互換に拡張する」General
  ・「RFC Viewerの紹介」iOS
  ・「ClangのModulesについて」tool
  ・「Cocoaでマルチウインドウ」Mac
  ・「古いシミュレータの捨て方」iOS

前回の勉強会で、NSURLConnectionにQueueを設定して主スレッド以外で動作させる方法を知り、早速、活用して喜んでいたのだが、NSURLSessionによって、もはや、過去のお話になってしまった事を知ってしまった。早速、活用してみよう。

APIの前方互換について、まだ、必要に迫られた事はないが、次のプロジェクトに必要になる可能性があるので復習を。

CloagのModules、ちょうど、フレームワークを作っているので、考慮してみよう。

Cocoaでマルチウィンドウ。最近、OS Xアプリケーションを作っていないので、作りたくなってきました。

古いシミュレータの捨て方、自分の環境を確認してみたところ、キャッシュにゴミが残っていた!これは助かる情報だ。

次回は、12月7日に松戸で開催予定だ。

関連情報 Cocoa勉強会
第62回 Cocoa勉強会 関東 - Mac/iOS開発勉強会
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]RFCViewer(並列処理の管理)

外部とのやり取りを管理する機能として、オートマティズムのコネクタを参考に、独自のカスタマイズを施している。

実際に利用していて分かった事は、個々の処理の実装は様々で、別スレッドから返ってくる場合があるが、それをメインスレッドに戻して、返す共通の機能としての役割があることだ。また、オートマティズムでは結果を通知で返していたが、これだと、要求との対応が弱くなるので、要求との対応を強くしたい場合は、要求時にBlocksを渡して、Blocksで応答する様に工夫してしてみた。

ヘッダーファイルを見てみよう。

#import <Foundation/Foundation.h>
#import "RFCResponseParser.h"
 
extern NSString *ConnectorDidBeginRfc;
extern NSString *ConnectorInProgressRfc;
extern NSString *ConnectorDidFinishRfc;
 
@interface Connector : NSObject
 
@property (assign, readonly, nonatomic, getter=isNetworkAccessing) BOOL networkAccessing;
 
+ (Connector *)sharedConnector;
- (void)rfcIndexWithCompletionHandler:(RFCResponseParserCompletionHandler)completionHandler;
- (void)rfcWithIndex:(NSUInteger)index completionHandler:(RFCResponseParserCompletionHandler)completionHandler;
- (void)cancelWithIndex:(NSUInteger)index;
- (void)cancelAll;
 
@end

リクエストのメソッドにcompletionHandlerというBlocksを渡せるようにして、これは、パーサに覚えさせておいて、処理が終わった際に呼ぶようにしている。

以下の実装部だ。

#import "Connector.h"
 
NSString    *ConnectorDidBeginRfc = @"ConnectorDidBeginRfc";
NSString    *ConnectorInProgressRfc = @"ConnectorInProgressRfc";
NSString    *ConnectorDidFinishRfc = @"ConnectorDidFinishRfc";
 
@interface Connector () 
@property (strong, nonatomic) NSOperationQueue  *queue;
@property (strong, nonatomic) NSMutableArray    *parsers;
- (void)_notifyRfcStatusWithParser:(RFCResponseParser *)parser;
@end
 
@implementation Connector
 
@synthesize queue = _queue;
@synthesize parsers = _parsers;
 
+ (Connector *)sharedConnector
{
    static Connector *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[Connector alloc] init];
    });
	
}
 
- (id)init
{
    DBGMSG(@"%s", __func__);
    self = [super init];
    if (self) {
        _queue = [[NSOperationQueue alloc]init];
        _parsers = [[NSMutableArray alloc] init];
    }
    return self;
}
 
- (void)dealloc
{
    DBGMSG(@"%s", __func__);
    self.queue = nil;
    self.parsers = nil;
}
 
- (BOOL)isNetworkAccessing
{
    return self.parsers.count > 0;
}
 
- (void)rfcIndexWithCompletionHandler:(RFCResponseParserCompletionHandler)completionHandler
{
    DBGMSG(@"%s", __func__);
    [self rfcWithIndex:0 completionHandler:completionHandler];
}
 
- (void)rfcWithIndex:(NSUInteger)index completionHandler:(RFCResponseParserCompletionHandler)completionHandler
{
    DBGMSG(@"%s", __func__);
    BOOL    networkAccessing = self.networkAccessing;
    
    RFCResponseParser   *parser = [[RFCResponseParser alloc] init];
    parser.index = index;
    parser.queue = self.queue;
    parser.delegate = self;
    parser.completionHandler = completionHandler;
    
    [parser parse];
    if (parser.error) {
        NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
        [userInfo setObject:parser forKey:@"parser"];
        [[NSNotificationCenter defaultCenter] postNotificationName:ConnectorDidFinishRfc
                                                            object:self
                                                          userInfo:userInfo];
        if (parser.completionHandler) {
            parser.completionHandler(parser);
        }
        return;
    }
    
    [self.parsers addObject:parser];
    
    if (networkAccessing != self.networkAccessing) {
        [self willChangeValueForKey:@"networkAccessing"];
        [self didChangeValueForKey:@"networkAccessing"];
    }
    
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:parser forKey:@"parser"];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:ConnectorDidBeginRfc object:self userInfo:userInfo];
}
 
- (void)cancelWithIndex:(NSUInteger)index
{
    DBGMSG(@"%s", __func__);
    NSArray *parsers = [self.parsers copy];
    for (RFCResponseParser *parser in parsers) {
        if (parser.index == index) {
            [parser cancel];
            
            NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
            [userInfo setObject:parser forKey:@"parser"];
            
            [[NSNotificationCenter defaultCenter] postNotificationName:ConnectorDidFinishRfc
                                                                object:self
                                                              userInfo:userInfo];
            
            [self willChangeValueForKey:@"networkAccessing"];
            [self.parsers removeObject:parser];
            [self didChangeValueForKey:@"networkAccessing"];
        }
    }
}
 
- (void)cancelAll
{
    DBGMSG(@"%s", __func__);
    for (RFCResponseParser *parser in self.parsers) {
        [parser cancel];
    }
    
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:self.parsers forKey:@"parsers"];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:ConnectorDidFinishRfc object:self userInfo:userInfo];
    
    [self willChangeValueForKey:@"networkAccessing"];
    [self.parsers removeAllObjects];
    [self didChangeValueForKey:@"networkAccessing"];
}
 
- (void)parser:(RFCResponseParser*)parser didReceiveResponse:(NSURLResponse*)response
{
    DBGMSG(@"%s", __func__);
}
 
- (void)parser:(RFCResponseParser *)parser didReceiveData:(NSData *)data
{
    DBGMSG(@"%s", __func__);
}
 
- (void)parserDidFinishLoading:(RFCResponseParser *)parser
{
    DBGMSG(@"%s", __func__);
    if ([self.parsers containsObject:parser]) {
        [self _notifyRfcStatusWithParser:parser];
    }
}
 
- (void)parser:(RFCResponseParser *)parser didFailWithError:(NSError*)error
{
    DBGMSG(@"%s", __func__);
    if ([self.parsers containsObject:parser]) {
        [self _notifyRfcStatusWithParser:parser];
    }
}
 
- (void)parserDidCancel:(RFCResponseParser *)parser
{
    DBGMSG(@"%s", __func__);
    if ([self.parsers containsObject:parser]) {
        [self _notifyRfcStatusWithParser:parser];
    }
}
 
- (void)_notifyRfcStatusWithParser:(RFCResponseParser *)parser
{
    DBGMSG(@"%s", __func__);
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:parser forKey:@"parser"];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:ConnectorDidFinishRfc
                                                        object:self
                                                      userInfo:userInfo];
    if (parser.completionHandler) {
        parser.completionHandler(parser);
    }
    
    [self willChangeValueForKey:@"networkAccessing"];
    [self.parsers removeObject:parser];
    [self didChangeValueForKey:@"networkAccessing"];
}
 
@end
ソースコード GitHubからどうぞ。
https://github.com/murakami/RFCViewer - GitHub
関連情報 新ToolBox 100の定石
iOS開発におけるパターンによるオートマティズム
Cocoa勉強会
MOSA
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

RFC Viewer開発を通して学ぶ!! iOS開発のパターン化

セミナーの講師をやることになりました。

http://atnd.org/events/43950
http://www.zusaar.com/event/1077005
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]RFCViewer(通信について)

オートマティズムのレスポンスパーサを参考に、独自のカスタマイズを施している。

通信のように外部とのやり取りを行う場合、Cocoa touchでは、実装された時期によって、自分でスレッド化したり、デリゲートだったり、キューやGCDだったりして、単純にAPIに沿うと、機能毎に実装が異なる事になってしまう。レスポンスパーサをそれを共通の形にすると自分は理解している。

ヘッダーファイルを見てみよう。
オートマティズムでは、デリゲートとなるコネクタに対して、デリゲート・メソッド呼び出しで結果を伝えているが、それを呼び出し元が用意したブロックを呼び出すようにカスタマイズをしている。

#import <Foundation/Foundation.h>
 
@class RFCResponseParser;
 
#define kRFCResponseParserNoError       0
#define kRFCResponseParserGenericError  1
 
typedef enum _RFCNetworkState {
    kRFCNetworkStateNotConnected = 0,
    kRFCNetworkStateInProgress,
    kRFCNetworkStateFinished,
    kRFCNetworkStateError,
    kRFCNetworkStateCanceled,
} RFCNetworkSate;
 
typedef void (^RFCResponseParserCompletionHandler)(RFCResponseParser *parser);
 
@protocol RFCResponseParserDelegate 
- (void)parser:(RFCResponseParser*)parser didReceiveResponse:(NSURLResponse*)response;
- (void)parser:(RFCResponseParser *)parser didReceiveData:(NSData *)data;
- (void)parserDidFinishLoading:(RFCResponseParser *)parser;
- (void)parser:(RFCResponseParser *)parser didFailWithError:(NSError*)error;
- (void)parserDidCancel:(RFCResponseParser *)parser;
@end
 
@interface RFCResponseParser : NSObject
 
@property (assign, readonly, nonatomic) RFCNetworkSate          networkState;
@property (assign, nonatomic) NSUInteger                        index;
@property (strong, nonatomic) NSError                           *error;
@property (strong, nonatomic) NSOperationQueue                  *queue;
@property (weak, nonatomic) id       delegate;
@property (copy, nonatomic)RFCResponseParserCompletionHandler   completionHandler;
@property (strong, readonly, nonatomic) NSArray                 *indexArray;
@property (strong, readonly, nonatomic) NSString                *rfc;
 
- (void)parse;
- (void)cancel;
 
@end

URL通信という事で、通常はメインスレッドでデリゲートの非同期の処理という事になるが、先日のCocoa勉強会で教えてもらった、キューを使っている。これで、メインスレッド以外のスレッドで動作する。

#import "Document.h"
#import "RFCResponseParser.h"
 
@interface RFCResponseParser () 
@property (assign, readwrite, nonatomic) RFCNetworkSate networkState;
@property (strong, readwrite, nonatomic) NSArray        *indexArray;
@property (strong, nonatomic) NSURLConnection           *urlConnection;
@property (strong, nonatomic) NSMutableData             *downloadedData;
- (void)_notifyParserDidFinishLoading;
- (void)_notifyParserDidFailWithError:(NSError*)error;
- (NSError *)_errorWithCode:(NSInteger)code localizedDescription:(NSString *)localizedDescription;
- (void)_parseIndexArray;
@end
 
@implementation RFCResponseParser
 
@synthesize networkState = _networkState;
@synthesize index = _index;
@synthesize error = _error;
@synthesize queue = _queue;
@synthesize delegate = _delegate;
@synthesize completionHandler = _completionHandler;
@synthesize indexArray = _indexArray;
@synthesize urlConnection = _urlConnection;
@synthesize downloadedData = _downloadedData;
 
- (id)init
{
    DBGMSG(@"%s", __func__);
    self = [super init];
    if (self) {
        _networkState = kRFCNetworkStateNotConnected;
        _index = 0;
        _error = nil;
        _queue = nil;
        _delegate = nil;
        _completionHandler = NULL;
        _indexArray = nil;
        _urlConnection = nil;
        _downloadedData = nil;
    }
    return self;
}
 
- (void)dealloc
{
    DBGMSG(@"%s", __func__);
    self.networkState = kRFCNetworkStateNotConnected;
    self.index = 0;
    self.error = nil;
    self.queue = nil;
    self.delegate = nil;
    self.completionHandler = NULL;
    self.indexArray = nil;
    self.urlConnection = nil;
    self.downloadedData = nil;
}
 
- (void)parse
{
    DBGMSG(@"%s", __func__);
    NSString    *urlString = nil;
    
    if (self.index == 0) {
        urlString = [Document sharedDocument].indexUrlString;
    }
    else {
        urlString = [[Document sharedDocument] rfcUrlStringWithIndex:self.index];
    }
    
    NSURLRequest    *urlRequest = nil;
    if (urlString) {
        NSURL   *url;
        url = [NSURL URLWithString:urlString];
        if (url) {
            urlRequest = [NSURLRequest requestWithURL:url];
        }
    }
    DBGMSG(@"%s urlString(%@)", __func__, urlString);
    
    if (! urlRequest) {
        self.networkState = kRFCNetworkStateError;
        self.error = [self _errorWithCode:kRFCResponseParserGenericError
                     localizedDescription:@"NSURLRequestの生成に失敗しました。"];
        return;
    }
    
    self.downloadedData = [[NSMutableData alloc] init];
        
    self.urlConnection = [[NSURLConnection alloc] initWithRequest:urlRequest
                                                         delegate:self
                                                 startImmediately:NO];
    [self.urlConnection setDelegateQueue:self.queue];
    
    [self willChangeValueForKey:@"networkState"];
    self.networkState = kRFCNetworkStateInProgress;
    [self didChangeValueForKey:@"networkState"];
    
    [self.urlConnection start];
}
 
- (void)cancel
{
    DBGMSG(@"%s", __func__);
    [self.urlConnection cancel];
    
    self.downloadedData = nil;
    
    [self willChangeValueForKey:@"networkState"];
    self.networkState = kRFCNetworkStateCanceled;
    [self didChangeValueForKey:@"networkState"];
    
    if ([self.delegate respondsToSelector:@selector(parserDidCancel:)]) {
        [self.delegate parserDidCancel:self];
    }
    
    self.urlConnection = nil;
}
 
- (NSString *)rfc
{
    DBGMSG(@"%s", __func__);
    NSString    *result = [[NSString alloc] initWithData:self.downloadedData encoding:NSUTF8StringEncoding];
    return result;
}
 
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
    DBGMSG( @"%s [Main=%@]", __FUNCTION__, [NSThread isMainThread] ? @"YES" : @"NO ");
    if ([self.delegate respondsToSelector:@selector(parser:didReceiveResponse:)]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate parser:self didReceiveResponse:response];
        });
    }
}
 
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
    DBGMSG( @"%s [Main=%@]", __FUNCTION__, [NSThread isMainThread] ? @"YES" : @"NO ");
    [self.downloadedData appendData:data];
    
    if ([self.delegate respondsToSelector:@selector(parser:didReceiveData:)]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate parser:self didReceiveData:data];
        });
    }
}
 
- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
    DBGMSG( @"%s [Main=%@]", __FUNCTION__, [NSThread isMainThread] ? @"YES" : @"NO ");
    
    [self willChangeValueForKey:@"networkState"];
    self.networkState = kRFCNetworkStateFinished;
    [self didChangeValueForKey:@"networkState"];
    
    [self _parseIndexArray];


    dispatch_async(dispatch_get_main_queue(), ^{
        [self _notifyParserDidFinishLoading];
    });


    self.urlConnection = nil;
}
 
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
    DBGMSG( @"%s [Main=%@]", __FUNCTION__, [NSThread isMainThread] ? @"YES" : @"NO ");
    self.error = error;
    
    [self willChangeValueForKey:@"networkState"];
    self.networkState = kRFCNetworkStateError;
    [self didChangeValueForKey:@"networkState"];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self _notifyParserDidFailWithError:error];
    });
    
    self.urlConnection = nil;
}
 
- (void)_notifyParserDidFinishLoading
{
    if ([self.delegate respondsToSelector:@selector(parserDidFinishLoading:)]) {
        [self.delegate parserDidFinishLoading:self];
    }
}
 
- (void)_notifyParserDidFailWithError:(NSError*)error
{
    if ([self.delegate respondsToSelector:@selector(parser:didFailWithError:)]) {
        [self.delegate parser:self didFailWithError:error];
    }
}
 
- (NSError *)_errorWithCode:(NSInteger)code localizedDescription:(NSString *)localizedDescription
{
    NSDictionary    *userInfo = [NSDictionary dictionaryWithObject:localizedDescription forKey:NSLocalizedDescriptionKey];
    NSError         *error = [NSError errorWithDomain:@"RFCViewer" code:code userInfo:userInfo];
    return error;
}
    :
@end

キューを使って別スレッドで動作させる場合に気をつけないと行けないのは、呼び出し元に戻す際は、メインスレッドでということだ。そこで、メインスレッドで動作する- _notifyParserDidFinishLoadingと- _notifyParserDidFailWithErrorのメソッドを用意して、それをdispatch_async()関数でメインスレッドで呼びようにしている。

別スレッドで動作する事の副作用で、読み込んだRFCのインデックス情報の解析には、それなりに時間があかかると思うが、この処理が別スレッドで実行される為、UIのレスポンスへの影響が小さくなっている。

- (void)_parseIndexArray
{
    DBGMSG( @"%s [Main=%@]", __FUNCTION__, [NSThread isMainThread] ? @"YES" : @"NO ");
    
    NSMutableArray  *indexArray = [[NSMutableArray alloc] init];
    
    NSString
	
	
	
    __block BOOL    isCreatedOn = NO;
    BOOL        isIndex = NO;
    NSMutableString *rfcString = nil;
    
	
										
	
	
	
        /* 行単位の取り出し */
		
		
		
        /* 改行文字削除 */
		
		
		
		
		
			
			
			
		
        else {
            parsedString = @"";
        }
        //DBGMSG(@"[%@]", parsedString);
        
        /* 更新日付に到着 */
        NSError *error = nil;
        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\(CREATED ON: (\\d{2}/\\d{2}/\\d{4})\\.\\)"
                                                                               options:NSRegularExpressionCaseInsensitive
                                                                                 error:&error];
        [regex enumerateMatchesInString:parsedString
                                options:0
                                  range:NSMakeRange(0, parsedString.length)
                             usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
                                 if (match.numberOfRanges) {
                                     isCreatedOn = YES;
                                 }
                             }];
        
        /* 目次に到達 */
        if ((isCreatedOn) && ([parsedString isEqualToString:@"RFC INDEX"])) {
            isIndex = YES;
        }
        else if (! isIndex) {
        }
        
        /* 区切り */
        else if ([parsedString isEqualToString:@""]) {
            if (rfcString) {
                /* 表題の取り出し */
                //DBGMSG(@"%@", rfcString);
                NSError *error = nil;
                NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^([0-9]{4}+)\\s(.+?\\.)"
                                                                                       options:NSRegularExpressionCaseInsensitive
                                                                                         error:&error];
                __block NSString    *rfcNumber = nil;
                __block NSString    *title = nil;
                [regex enumerateMatchesInString:rfcString
                                        options:0
                                          range:NSMakeRange(0, rfcString.length)
                                     usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
                                         //NSRange    matchRange = [match range];
                                         NSRange    firstHalfRange = [match rangeAtIndex:1];
                                         NSRange    secondHalfRange = [match rangeAtIndex:2];
                                         rfcNumber = [rfcString substringWithRange:firstHalfRange];
                                         title = [rfcString substringWithRange:secondHalfRange];
                                     }];
                RFC *rfc = [[RFC alloc] init];
                rfc.rfcNumber = rfcNumber;
                rfc.title = title;
                //DBGMSG(@"%@ : %@", rfcNumber, rfc.title);
                [indexArray addObject:rfc];
            }
            rfcString = nil;
        }
        
        /* 先頭 */
        else {
            NSRange match = [parsedString rangeOfString:@"^[0-9]{4}+\\s" options:NSRegularExpressionSearch];
            if (match.location != NSNotFound) {
                //DBGMSG(@"++++先頭");
                rfcString = [[NSMutableString alloc] initWithString:parsedString];
            }
            else if (rfcString) {
                [rfcString appendString:parsedString];
            }
        }
		
		
	
    self.indexArray = indexArray;
}
ソースコード GitHubからどうぞ。
https://github.com/murakami/RFCViewer - GitHub
関連情報 新ToolBox 100の定石
iOS開発におけるパターンによるオートマティズム
Cocoa勉強会
MOSA
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]RFCViewer(モデルについて)

アプリケーション共通のデータをDocumentクラスで管理するようにしている。

#import <Foundation/Foundation.h>
#import "RFC.h"
 
@interface Document : NSObject
 
@property (strong, nonatomic) NSString              *version;
@property (strong, readonly, nonatomic) NSString    *indexUrlString;
@property (strong, nonatomic) NSArray               *indexArray;
 
+ (Document *)sharedDocument;
- (void)load;
- (void)save;
- (NSString *)rfcUrlStringWithIndex:(NSUInteger)index;
 
@end
#import "Document.h"
 
@interface Document ()
@property (strong, readwrite, nonatomic) NSString   *indexUrlString;
@property (strong, nonatomic) NSString              *baseUrlString;
- (void)_clearDefaults;
- (void)_updateDefaults;
- (void)_loadDefaults;
@end
 
@implementation Document
 
@synthesize version = _version;
@synthesize indexUrlString = _indexUrlString;
@synthesize indexArray = _indexArray;
@synthesize baseUrlString = _baseUrlString;
 
+ (Document *)sharedDocument;
{
    static Document *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[Document alloc] init];
    });
	
}
 
- (id)init
{
    DBGMSG(@"%s", __func__);
    self = [super init];
    if (self) {
        _version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
        _indexUrlString = @"http://www.rfc-editor.org/rfc/rfc-index.txt";
        _indexArray = [[NSArray alloc] init];
        _baseUrlString = @"http://www.ietf.org/rfc";
    }
    return self;
}
 
- (void)dealloc
{
    DBGMSG(@"%s", __func__);
    self.version = nil;
    self.indexUrlString = nil;
    self.indexArray = nil;
    self.baseUrlString = nil;
}
 
- (void)load
{
    DBGMSG(@"%s", __func__);
    [self _loadDefaults];
}
 
- (void)save
{
    DBGMSG(@"%s", __func__);
    [self _updateDefaults];
}
 
- (NSString *)rfcUrlStringWithIndex:(NSUInteger)index
{
    NSString *urlString = [[NSString alloc] initWithFormat:@"%@/rfc%04u.txt", self.baseUrlString, index];
    return urlString;
}
 
- (void)_clearDefaults
{
    DBGMSG(@"%s", __func__);
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"version"]) {
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"version"];
    }
}
 
- (void)_updateDefaults
{
    DBGMSG(@"%s", __func__);
    NSString    *versionString = nil;
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"version"]) {
        versionString = [[NSUserDefaults standardUserDefaults] objectForKey:@"version"];
    }
    if ((versionString == nil) || ([versionString compare:self.version] != NSOrderedSame)) {
        [[NSUserDefaults standardUserDefaults] setObject:self.version forKey:@"version"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}
 
- (void)_loadDefaults
{
    DBGMSG(@"%s", __func__);
    NSString    *versionString = nil;
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"version"]) {
        versionString = [[NSUserDefaults standardUserDefaults] objectForKey:@"version"];
    }
    if ((versionString == nil) || ([versionString compare:self.version] != NSOrderedSame)) {
        /* バージョン不一致対応 */
    }
    else {
        /* 読み出し */
    }
}
 
@end

このDocumentクラスのインスタンスへのアクセスだが、以前は、アプリケーションのデリゲートのインスタンスにして、以下のようにアクセスしていたが、冗長なのでシングルトンとした。

#import "AppDelegate.h"
 
AppDelegate *appl = nil;
appl = (AppDelegate *)[[UIApplication sharedApplication] delegate];
Document *document = appl.document;

シングルトンの為のクラス・メソッドも、以前は、以下のように実装していたが、GCDのdispatch_once関数を使った実施に変更している。

static Document *_sharedInstance = nil;
+ (Document *)sharedDocument
{
	
		
	
	
}

そして、Documentクラスには、必ず、NSUserDefaultsを利用した初期設定値の保存と読み込みを用意して、最低でもバージョン番号を保存するようにしている。これは、後でNSUserDefaultsを利用したくなった際に、以前のバージョンでは保存していないと、バージョン際が発生した際の判断が面倒になるためだ。
- _updateDefaultsメソッドで保存し、- _loadDefaultsメソッドで読み込んでいる。

NSUserDefaultsの読み書きメソッドは、- loadメソッドと- saveメソッドから呼ぶようにして、このメソッドをアプリケーション・デリゲートのアプリ起動時やバックグラウンドに遷移するタイミングで呼ばれるようにしている。

@implementation AppDelegate
 
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    DBGMSG(@"%s", __func__);
    [[Document sharedDocument] load];
    [[Connector sharedConnector] addObserver:self
                                  forKeyPath:@"networkAccessing"
                                     options:0
                                     context:NULL];
    return YES;
}
 
:
 
- (void)applicationWillResignActive:(UIApplication *)application
{
    DBGMSG(@"%s", __func__);
    [[Document sharedDocument] save];
}
 
:
@end
ソースコード GitHubからどうぞ。
https://github.com/murakami/RFCViewer - GitHub
関連情報 新ToolBox 100の定石
iOS開発におけるパターンによるオートマティズム
Cocoa勉強会
MOSA
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]RFCViewer

これまでiOSアプリケーション開発をしてきた経験から、他でも使えるコードをパターン化して、新規プロジェクトの雛形となるコードを用意したので紹介する。

再利用可能なアプリケーションは、RFCViewerという名前で、GitHubから自由に入手できるようにしている。

RFCViewer

詳細な内容は、今後、【Cocoa練習帳】で発表する予定だが、今回は簡単な概要を説明する。

このアプリケーションは、RFC-Editorのサイトからインデックスを取得して、インデックスに対応したRFC文書を表示するアプリケーションだ。

MVCのデザインパターンで実装されていて、M(モデル)に対応するのが、Documentクラスだ。RFC-Editorサイトの情報(URL)や、取得したインデックスやRFC文書を管理している。
Documentクラスについては、以前、mosa entranceで議論させていただいたのだが、そのとき、アドバイスをいただいた方々、ありがとうございます。

通信部分については、『iOS開発におけるパターンによるオートマティズム』のネットワークのパターン(コネクタとレスポンスパーサ)を参考にして、実際の開発現場で施したカスタマイズを盛り込んでいる。
大げさに感じる方がいるかもしれないが、一口に通信といっても、TCP/IPからBluetooth、外部機器等、様々で、それぞれ、デリゲートだったり、スレッド化したり、キューだったりと非同期/並列化の方法が異なり、それらと同じ構造で扱えるのが、このパターンの魅力だ。

INDEX

詳細

この雛形アプリケーションを製作するにあたって、参考にしたサイトを以下の関連情報で紹介にしているので、参考にして欲しい。

ソースコード GitHubからどうぞ。
https://github.com/murakami/RFCViewer - GitHub
関連情報 新ToolBox 100の定石
iOS開発におけるパターンによるオートマティズム
Cocoa勉強会
MOSA
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)
iOS開発におけるパターンによるオートマティズム/ビー・エヌ・エヌ新社
¥2,940
Amazon.co.jp

関東第61回Cocoa勉強会

関東第61回Cocoa勉強会が、新宿三丁目の貸し会議室で開催された。

発表は、メインスレッド外でNSURLConnection、ARCにおけるRetain Countのデバッグ、iOS6からの状態の保存と復元、Cocoa PodsとCocoa子ネタ。

NSURLConnection、早速、自分のプロジェクトでも今回の発表の内容を活用しよう。

Retain Countのデバッグ、DTraceの利用について、試してみようかな。

Cocoa Pods、いいじゃないですか。そして、プロジェクトの構成は、プロジェクト分割を考えていたので、参考になるな。

子ネタも、役立つ時がきそうだ。

Cocoa勉強会

関連情報 Cocoa勉強会
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]Tangiblock

Tangiblock説明会に参加してきた。

Tangiblockとは、iOSの指にそうさを疑似る仕組みを使った、50個のブロックとiPadアプリケーションを組み合わせて使うもので、今回、SDKの提供により第三者にTangiblockを使ったアプリケーションの開発の可能性が生まれたものだ。

なかなか、面白いものだと思うので、興味がある方は、チャレンジしてみてはどうか?

関連情報 Tangiblock
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]状態の保存と復元

iOS6から導入された状態の保存と復元を試してみた。この状態の保存と復元は多くのパターンがあるのだが、今回のその一部のみだ。

UIKitの状態保存システムは、バックグラウンド実行しているアプリケーションが、リソース不足等の理由からOSによって停止させられた場合に、フォアグラウンド状態となった際に、停止前の状態を再現する機能のようだ。なので、ユーザが強制的に停止させた場合は、対象で機能しないらしい。

保存と復元の対象はビューコントローラで、ビューコントローラとその配下のビューの状態が保存と復元される。そして、モデルについては対象としないようにするのが利用するコツのようだ。

状態の保存と復元の対応によって、たとえば、アプリケーション・デリゲートのメソッドは、呼ばれるタイミングが、保存と復元の前なのか、後なのかを意識する必要がある。

/* 状態情報復元前の初期化 */
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    DBGMSG(@"%s", __func__);
    [[Document sharedDocument] load];
    return YES;
}
 
/* 状態情報復元後の初期化 */
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    DBGMSG(@"%s", __func__);
    return YES;
}
 
/* 状態情報保存前のモデル保存 */
- (void)applicationWillResignActive:(UIApplication *)application
{
    DBGMSG(@"%s", __func__);
    [[Document sharedDocument] save];
}

この機能を有効化は、アプリケーションのデリゲートで行われる。


/* 状態情報保存の有効化 */
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder
{
    return YES;
}
 
/* 状態情報復元の有効化 */
- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder
{
    return YES;
}

Storyboardで、ビューコントローラにRestoration IDというのが追加されているのが確認できるが、このIDで保存と復元が行われる。同じビューコントローラが複数生成され、それを区別する場合は、インスタンス毎にIDを変える等の工夫が必要なようだ。

ビューコントローラの状態の保存と復元は、その為のメソッドを実装する。

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSLog(@"MasterViewController: encodeRestorableStateWithCoder");
    [super encodeRestorableStateWithCoder:coder];
    [[Document sharedDocument] save];
    [coder encodeInteger:self.detailItemIndex forKey:kUnsavedDetailItemIndexKey];
}
 
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSLog(@"MasterViewController: decodeRestorableStateWithCoder");
    [super decodeRestorableStateWithCoder:coder];
    self.detailItemIndex = [coder decodeBoolForKey:kUnsavedDetailItemIndexKey];
    [self configureView];
}

上記の例では、配列型のモデルのインデックスを保存していて、復元時は、そのインデックスで停止前のデータにアクセスするようにしている。

実際に動作確認したいのだが、どうやったら、OSによって停止させられるのかよく分からなかった為。これで上手くいくのは、まだ試していない。残念!

ソースコード GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/StateRestoration - GitHub
関連情報 iOS App Programming Guide
State Restoration
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)