[cocoa][swift]マイDocumentクラス

<p>一月の勉強会でMVCについてディスカッションすることになった。そこで場が盛り上がるよう、ネタとして自分がよく採用するマイDocumentクラスに発表する。</p>

<p>アプリケーションの設計法としてMVCが話題となることが多いが、それは、MVCはデザイン・パターンが話題になる以前のもので、今のデザイン・パターンから見ると複数のパターンが組み合わさった大きな枠組みのものだというのも理由としてあるのかな？</p>

<p>早速本題に入る。Appleの文書で説明されている伝統的なMVCは以下のとおり。</p>

![Traditional version of MVC as a compound pattern.png](https://qiita-image-store.s3.amazonaws.com/0/12540/4e21148f-c4dc-c482-71cf-670dbb10b737.png)

<p>これは、<a href="https://www.ics.uci.edu/~redmiles/ics227-SQ04/papers/KrasnerPope88.pdf">この論文</a>でも説明されている。</p>

<p>そして、Cocoa版は以下となる。</p>

![Cocoa version of MVC as a compound design pattern.png](https://qiita-image-store.s3.amazonaws.com/0/12540/0a33523d-cb53-0ec4-8632-cf5cc747ab9b.png)

<p>ViewとModelは直接やりとりしない。間にControllerを挟んでいるのが特徴だ。このControllerだが、一種類でない。NSApplicationDelegateだったりNSDOcumentだったりNSControllerだったりNSViewControllerだったり、アプリケーション独自のクラスだったりする。</p>

<p>iOS開発を始めたときから独自のDocumentクラスを用意するようにしている。</p>

<pre><code>@interface Document : NSObject
@property (strong, nonatomic) NSString              *version;
 
+ (Document *)sharedDocument;
- (void)load;
- (void)save;
@end
</code></pre>

<pre><code>@interface Document ()
- (void)_clearDefaults;
- (void)_updateDefaults;
- (void)_loadDefaults;
- (NSString *)_modelDir;
- (NSString *)_modelPath;
@end
 
@implementation Document
 
@synthesize version = _version;
 
+ (Document *)sharedDocument;
{
    static Document *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[Document alloc] init];
    });
    return _sharedInstance;
}
 
- (id)init
{
    self = [super init];
    if (self) {
        _version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
    }
    return self;
}
 
- (void)dealloc
{
    self.version = nil;
}
 
- (void)load
{
    [self _loadDefaults];
    
    NSString    *modelPath = [self _modelPath];
    if ((! modelPath) || (! [[NSFileManager defaultManager] fileExistsAtPath:modelPath])) {
        return;
    }
}
 
- (void)save
{
    [self _updateDefaults];
    
    NSFileManager   *fileManager = [NSFileManager defaultManager];
    
    NSString    *modelDir = [self _modelDir];
    if (![fileManager fileExistsAtPath:modelDir]) {
        NSError *error = nil;
        [fileManager createDirectoryAtPath:modelDir
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:&error];
    }
    
    NSString    *modelPath = [self _modelPath];
    [NSKeyedArchiver archiveRootObject:self.indexArray toFile:modelPath];
}
 
- (void)_clearDefaults
{
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"version"]) {
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"version"];
    }
}
 
- (void)_updateDefaults
{
    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
{
    NSString    *versionString = nil;
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"version"]) {
        versionString = [[NSUserDefaults standardUserDefaults] objectForKey:@"version"];
    }
    if ((versionString == nil) || ([versionString compare:self.version] != NSOrderedSame)) {
        /* バージョン不一致対応 */
    }
    else {
        /* 読み出し */
    }
}
 
- (NSString *)_modelDir
{
    NSArray    *paths;
    NSString   *path;
    paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    if ([paths count] < 1) {
        return nil;
    }
    path = [paths objectAtIndex:0];
    
    path = [path stringByAppendingPathComponent:@".model"];
    return path;
}
 
- (NSString *)_modelPath
{
    NSString   *path;
    path = [[self _modelDir] stringByAppendingPathComponent:@"model.dat"];
    return path;
}
@end
</code></pre>

<p>macOSのDocument-Based. Applicationsについて説明する。</p>

<p>主要なクラスは、以下の3つ。</p>

<table border="1">
<tr>
	<th>クラス</th>
	<th>内容</th>
</tr>
<tr>
	<td>NSDocument</td>
	<td>データ管理。</td>
</tr>
<tr>
	<td>NSWindowController</td>
	<td>ウィンドウ管理。</td>
</tr>
<tr>
	<td>NSDocumentController</td>
	<td>ドキュメント管理。</td>
</tr>
</table>

<p>ファイルとDocument、Modelの関係を図だ。</p>

![Document.png](https://qiita-image-store.s3.amazonaws.com/0/12540/ada4f616-1f14-31ca-ce53-70e69f13ab12.png)

<p>macOSのDocument-Based Applicationのクラスの関係を図示する。</p>

![Document-Based Apps.png](https://qiita-image-store.s3.amazonaws.com/0/12540/b48df69c-3494-973a-310c-93fca9628a7e.png)

![Key objects in a document-based app.png](https://qiita-image-store.s3.amazonaws.com/0/12540/48d95cb2-5021-b39e-673c-11f2bb500c34.png)

【関連情報】
<a href="https://qiita.com/advent-calendar/2018/cocoa">Cocoa Advent Calendar 2018</a><br />
<a href="https://cocoa-kanto.connpass.com/event/111294/">Cocoa.swift 2019-01</a><br />
<a href="https://cocoa-kanto.connpass.com/">Cocoa.swift</a><br />
<a href="https://www.facebook.com/groups/cocoa.kanto/">Cocoa勉強会 関東</a><br />
<a href="http://www.bitz.co.jp/weblog/">Cocoa練習帳</a><br />
<a href="https://qiita.com/m_yukio">Qiita</a><br />