iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
関東第61回Cocoa勉強会が、新宿三丁目の貸し会議室で開催された。
発表は、メインスレッド外でNSURLConnection、ARCにおけるRetain Countのデバッグ、iOS6からの状態の保存と復元、Cocoa PodsとCocoa子ネタ。
NSURLConnection、早速、自分のプロジェクトでも今回の発表の内容を活用しよう。
Retain Countのデバッグ、DTraceの利用について、試してみようかな。
Cocoa Pods、いいじゃないですか。そして、プロジェクトの構成は、プロジェクト分割を考えていたので、参考になるな。
子ネタも、役立つ時がきそうだ。
これまでiOSアプリケーション開発をしてきた経験から、他でも使えるコードをパターン化して、新規プロジェクトの雛形となるコードを用意したので紹介する。
再利用可能なアプリケーションは、RFCViewerという名前で、GitHubから自由に入手できるようにしている。
詳細な内容は、今後、【Cocoa練習帳】で発表する予定だが、今回は簡単な概要を説明する。
このアプリケーションは、RFC-Editorのサイトからインデックスを取得して、インデックスに対応したRFC文書を表示するアプリケーションだ。
MVCのデザインパターンで実装されていて、M(モデル)に対応するのが、Documentクラスだ。RFC-Editorサイトの情報(URL)や、取得したインデックスやRFC文書を管理している。
Documentクラスについては、以前、mosa entranceで議論させていただいたのだが、そのとき、アドバイスをいただいた方々、ありがとうございます。
通信部分については、『iOS開発におけるパターンによるオートマティズム』のネットワークのパターン(コネクタとレスポンスパーサ)を参考にして、実際の開発現場で施したカスタマイズを盛り込んでいる。
大げさに感じる方がいるかもしれないが、一口に通信といっても、TCP/IPからBluetooth、外部機器等、様々で、それぞれ、デリゲートだったり、スレッド化したり、キューだったりと非同期/並列化の方法が異なり、それらと同じ構造で扱えるのが、このパターンの魅力だ。
この雛形アプリケーションを製作するにあたって、参考にしたサイトを以下の関連情報で紹介にしているので、参考にして欲しい。
アプリケーション共通のデータを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];
});
return _sharedInstance;
}
- (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
{
if (!_sharedInstance) {
_sharedInstance = [[Document alloc] init];
}
return _sharedInstance;
}
そして、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
オートマティズムのレスポンスパーサを参考に、独自のカスタマイズを施している。
通信のように外部とのやり取りを行う場合、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 *indexString = nil;
NSString *parsedString = nil;
NSRange range, subRange;
NSUInteger length;
__block BOOL isCreatedOn = NO;
BOOL isIndex = NO;
NSMutableString *rfcString = nil;
indexString = [[NSString alloc] initWithData:self.downloadedData
encoding:NSUTF8StringEncoding];
length = [indexString length];
range = NSMakeRange(0, length);
while (0 < range.length) {
/* 行単位の取り出し */
subRange = [indexString lineRangeForRange:NSMakeRange(range.location, 0)];
parsedString = [indexString substringWithRange:subRange];
/* 改行文字削除 */
NSCharacterSet *chSet = nil;
NSScanner *scanner = nil;
chSet = [NSCharacterSet characterSetWithCharactersInString:@"\r\n"];
scanner = [NSScanner scannerWithString:parsedString];
if (![scanner isAtEnd]) {
NSString *line = nil;
[scanner scanUpToCharactersFromSet:chSet intoString:&line];
parsedString = line;
}
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];
}
}
range.location = NSMaxRange(subRange);
range.length -= subRange.length;
}
self.indexArray = indexArray;
}