iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
一月の勉強会でMVCについてディスカッションすることになった。そこで場が盛り上がるよう、ネタとして自分がよく採用するマイDocumentクラスに発表する。
アプリケーションの設計法としてMVCが話題となることが多いが、それは、MVCはデザイン・パターンが話題になる以前のもので、今のデザイン・パターンから見ると複数のパターンが組み合わさった大きな枠組みのものだというのも理由としてあるのかな?
早速本題に入る。Appleの文書で説明されている伝統的なMVCは以下のとおり。
これは、この論文でも説明されている。
そして、Cocoa版は以下となる。
ViewとModelは直接やりとりしない。間にControllerを挟んでいるのが特徴だ。このControllerだが、一種類でない。NSApplicationDelegateだったりNSDOcumentだったりNSControllerだったりNSViewControllerだったり、アプリケーション独自のクラスだったりする。
iOS開発を始めたときから独自のDocumentクラスを用意するようにしている。
@interface Document : NSObject
@property (strong, nonatomic) NSString *version;
+ (Document *)sharedDocument;
- (void)load;
- (void)save;
@end
@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
macOSのDocument-Based. Applicationsについて説明する。
主要なクラスは、以下の3つ。
クラス | 内容 |
---|---|
NSDocument | データ管理。 |
NSWindowController | ウィンドウ管理。 |
NSDocumentController | ドキュメント管理。 |
ファイルとDocument、Modelの関係を図だ。
macOSのDocument-Based Applicationのクラスの関係を図示する。
JSON(JavaScript Object Notation)は、通信などで利用されているデータ交換フォーマットだ。人によって読み書きが容易で、かつ、計算機にとって簡単に解釈や生成が行える。また、システムの標準的なライブラリが対応していて導入が楽という利点もあり、広く利用されている。
JSONにかわるものとしてMessage Packというフォーマとがあって、コンパクトで率的に扱えるということで、自分の周りでは使われだしている。ただ、macOS/iOSプログラマーとして難点なのは複数のライブラリが存在するのだが、安心して利用できる決定打がないということだ。また、IDL (Interface Definition Language) による読み書きコードの生成の需要があるのだが、Message Packは利用側が独自に用意するしかないということだ。
IDLが利用できるデータ交換フォーマットは色々あるのだが、Googleがオープンソースとして公開してたProtocol Buffersというデータ交換フォーマットのSwiftライブラリをAppleが用意していることを知った。そこで、どのような使いごこちなのか試してみることにした。
この記事を書いている時に最新はProtocol Buffers v3.6.1なので、protoc-3.6.1-osx-x86_64.zip をダウンロドする。
これを例えば、~/binにコピーして、~/.bash_profileなどでパスを通しておく。
PATH="$HOME/bin/protoc-3/bin:$HOME/bin:$PATH"
次に、Swift Protobuf プラグインを組み込む。
最も手軽な方法は、Homebrewだと思う。
$ brew install swift-protobuf
でも、今回はgithubからクローンするやり方を選んだ。
$ git clone https://github.com/apple/swift-protobuf.git
$ cd swift-protobuf
リリース・バージョンを確認する。
$ git tag -l
この記事を書いている時に最新だった1.3.1を選んでビルドする。
$ git checkout tags/1.3.1
$ swift build -c release -Xswiftc -static-stdlib
.build/release に、protoc-gen-swift というファイルが生成されているので、これを例えば ~/bin にコピーする。
以下のようなprotoファイル"my.proto"を用意する。
syntax = "proto3";
message BookInfo {
int64 id = 1;
string title = 2;
string author = 3;
}
これを以下のコマンドでswiftコードに変換する。
$ protoc --swift_out=. my.proto
my.pb.swift というファイルが生成されているはずだ。
次に、自分のXcodeプロジェクトに組み込む。
CocoaPodsやCarthageを利用する方法があるが、ここでは、Swift ProtobufのXcodeプロジェクトを自分のXcodeプロジェクトに組み込む方法でやることにする。
自分のXcodeプロジェクトに組み込むと、TARGETSのBuild PhasesのTarget DependencesにmacOS/iOS/tvOS/watchOS毎に追加できるようになっているので、必要なものを追加する。
そして、先ほど、生成した my.pb.swift を追加する。
すると、以下のように呼び出せるはずだ。
// Create a BookInfo object and populate it:
var info = BookInfo()
info.id = 1734
info.title = "Really Interesting Book"
info.author = "Jane Smith"
// As above, but generating a read-only value:
let info2 = BookInfo.with {
$0.id = 1735
$0.title = "Even More Interesting"
$0.author = "Jane Q. Smith"
}
print("\(info2)")
// Serialize to binary protobuf format:
let binaryData: Data = try! info.serializedData()
// Deserialize a received Data object from `binaryData`
let decodedInfo = try! BookInfo(serializedData: binaryData)
print("\(decodedInfo)")
// Serialize to JSON format as a Data object
let jsonData: Data = try! info.jsonUTF8Data()
// Deserialize from JSON format from `jsonData`
let receivedFromJSON = try! BookInfo(jsonUTF8Data: jsonData)
print("\(receivedFromJSON)")
有名な『Algorithms + Data Structures = Programs』の後半を独立して誕生した『COMPILERBAU:』を翻訳した『翻訳系構成法序論』を今の電子計算機環境で取り組んでみた。
使用するプログラミング言語Swiftを選択したのだが、コンパイラの実装には少々向いていない部分があるので、まずは、一文字読み込んで処理するサンプルを記述してみた。
import Foundation
let parser = Parser()
import Foundation
class Parser {
var ch: Character = "\0"
var lineString: String = ""
init() {
readChar()
S()
}
func readChar() {
lineString = readLine()!
ch = lineString[lineString.index(lineString.startIndex, offsetBy:0)]
lineString = String(lineString.suffix(lineString.count - 1))
}
/*
開式記号に対応する手続き。
*/
func S() {
}
}
それでは、教科書のサンプルコードを記述してみよう。
以下の約束事があるとする。
A="x"|"("B")".
B=AC.
C={"+"A}.
これは以下のようになる。
x
(x)
(x+x)
((x))
((x+(x+x)))
これを実装してみると以下となる。
/* 分析子 */
class Parser {
var ch: Character = "\0"
var lineString: String = ""
init() {
print("\(#function)")
readChar()
A()
}
func A() {
//print("\(#function)")
if ch == "x" {
readChar()
}
else if ch == "(" {
readChar()
A()
while ch == "+" {
readChar()
A()
}
if ch == ")" {
readChar()
}
else {
error()
}
}
else {
error()
}
}
func readChar() {
if lineString.count <= 0 {
lineString = readLine()!
}
ch = lineString[lineString.index(lineString.startIndex, offsetBy:0)]
lineString = String(lineString.suffix(lineString.count - 1))
print(ch)
}
func error() {
print("error: \(#function)")
exit(-1)
}
}
ここまでは、約束事を愚直にコードで処理しているという感じだ。