hylom'sWeblog

- hylom's blog
| about hylom / hylom.net | old contents | login

macOSアプリ開発でStoryboard+Core Dataを使うときのハマり話

posted on 2017/04/09 22:55:35

 ふと思い立って久しぶりにmacOSアプリ開発を始めて見たのだが、Core Dataで管理しているデータを(CocoaBindingを使って) Storyboardを使って作った画面上に表示するところでハマったうえにググっても情報を探すのが大変だったのでメモ。

参考文献

作業と疑問点

 Xcode(7.2.1)でOS Xの「Cocoa Application」を選択してテンプレートからプロジェクトを作成。このとき「Use Storyboards」、「Create Document-Based Application」、「Use Core Data」にチェックを入れておく。続いてAppleのCore Dataプログラミングガイドの「管理オブジェクトモデルの作成」に従ってオブジェクトモデルを作成する。

 さらにこのドキュメントでは「Core Dataスタックの初期化」を行うよう記されていて、NSManagedObjectContext、NSPersistentStoreCoordinator、NSManagedObjectModelの3つが必要と記述されている。しかし、初期化コードとしてDataControllerクラスの実装例もあるのだが、このオブジェクトをどのオブジェクトが所持すべきなのか(ビューなのかドキュメントなのか)が記載されていない。

 プロジェクト作成時に「Use Core Data」にチェックを入れておくと、ドキュメントの基底クラスとしてNSPersistentDocumentが使われるのだが、ドキュメントを見る限り、自動的にCore Data関連の初期化を行って「ready-to-use」のNSManagedObjectContextのインスタンスを提供すると書いてある。この解釈だと、自前でCore Dataスタックの初期化をする必要はなさそうである。

 「Core Data の使い方(1)」ページではViewController内でNSManagedObjectContextのインスタンスを作成し、ここでCore Dataの初期化を行っているのだが、本来Core Dataの管理はDocument(MVCにおけるModel)の仕事であるべきで、ViewController内でこの作業を行うのはよろしくないのではないか、という疑問があるし、前述の通りNSPersistentDocumentを利用する場合この処理は不要であるはずである。

 しかし、Documentが持つNSManagedObjectのインスタンスをArray Controllerに参照させる手段が分からない。DocumentにArray Controllerのoutletを作成できれば、そのオブジェクトのsetManagedObjectContextを使ってNSManagedObjectContextインスタンスを渡すことはできるのだが、StoryboardではDocumentにArray Controllerのoutletを作成することもできない。

 ちなみにArray Controllerを使ってTable ViewとArray Controllerをバインドしただけだと、Array ControllerにNSManagedObjectContextが関連付けられていないため「cannot perform operation without a managed object context」というメッセージが表示される。

解決法

 StackOverflowの「How do you bind a storyboard view to a Core Data entity when using NSDocument?」にそのものずばりの質問と回答があった。結論的には、まずDocument.swiftのmakeWindowControllers()の末尾に下記を追加する。

windowController.contentViewController!.representedObject = windowController.document

 これでView ControllerのrepresentedObjectがDocumentを参照するようになる。

 続いてArray ControllerのView Controllerで、Managed Object Contextで「Bind to」にチェックを入れ、「View Controller」を指定する。Model Key Pathには「representedObject.managedObjectContext」を指定する。以上で完了。ただ、このときModel Key Pathに「!」が表示されるのがちょっと気持ち悪い。

まとめ

  • 「Use Core Data」を選択してプロジェクトのテンプレートを作った場合、独自に初期化コードを書く必要は無い
  • DocumentからView Controllerにアクセスするのは面倒臭い

 なおこの辺の作業を行ったコードはhttps://github.com/hylom/ttune/tree/76d43fe476c24f064aee51b7748529aa82a309d1です。

Python 2.6/2.7のxmlrpclibでxml.parsers.expat.ExpatErrorが出た場合の対処

posted on 2013/06/26 22:40:25

 Python 2.6系および2/7系のxmlrpclibで、サーバーからはどう見ても正しいXMLが返ってきているはずなのにxml.parsers.expat.ExpatErrorが出た場合の対処方法メモ。

 xmlrpclibでは、サーバーから受け取ったXMLデータを一定サイズごとに分割してXMLパーサーに投入する、という処理を行っている。このときXMLパーサーにExpatを使っていると、受け取ったデータが分割される位置によっては、不正なXMLと認識されて以下のようにエラーになる模様(DebianのPython 2.6.6とMac OS XのPython 2.7.2で確認)。Expatを使わないようにするとこの問題は発生しない。

  File "/Users/hylom/repos/anpanel/myxmlrpclib.py", line 559, in feed
    self._parser.Parse(data, 0)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 30, column 114

 たぶん挙動的にExpatのバグのようだが、それを修正する気力もなかったので、とりあえずxmlrpclibでExpatを使わないようにすることで対処。たぶんパース速度は低下すると思われるが。具体的には、xmlrpclibをimportしたのち、XMLパーサーを取得するxmlrpclib.getparser関数を以下のようにして置き換える。

import xmlrpclib

# xmlrpclib's ExpatParser has bug, so do not use
org_getparser = xmlrpclib.getparser
def mygetparser(use_datetime=0):
    (p, t) = org_getparser(use_datetime)
    # if parser is ExpatParser, replace to SlowParser
    if isinstance(p, xmlrpclib.ExpatParser):
        p = xmlrpclib.SlowParser(p._target)
    return (p, t)
xmlrpclib.getparser = mygetparser

 ここでは、getparser関数の戻り値がExpatParserクラスのインスタンスだった場合、SlowParserクラスのインスタンスに置き換えるという処理をやっている。当然ながらかなりのdirty hackなので利用はおすすめしない。

Node.jsからMongoDBにバイナリデータを格納する

posted on 2013/04/16 02:25:39

 MongoDBにデータを格納する場合、格納するデータをプロパティとして持つオブジェクトを引数にコレクションのinsertメソッドを実行する。もしバイナリデータをデータベースに格納したい場合、バイナリデータをBuffer型のオブジェクトとして与えればよい。ただし、MongoDBに格納するドキュメント(データを格納する単位。SQLデータベースの1行に相当する)には最大4MBというサイズ制限がある。そのため、格納できるバイナリデータの上限は4MBだ。

 次のサンプルコードは、引数で指定したファイルを読み込んでデータベースに格納するものだ。

 データベースに格納するオブジェクトを用意しているのが42行〜45行の部分だ。ここでdataオブジェクトは読み出したデータを格納しているBuffer型のオブジェクトとなっている。

 ちなみに、格納されたデータをmongoシェルで確認すると、BASE64でエンコードされた形で表示される。

$ node store-to-mongodb.js ./test/sample.JPG
insert succeeded.
 
$ mongo test01
> db.images.find()
{ "filename" : "sample.JPG", "data" : BinData(0,"/9j/4AAQSkZJRgABAQEAtAC0AAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
:
:

 逆にデータベースからバイナリデータを読み出した場合、読み出したバイナリデータはmongodb.Binary型のオブジェクトに格納される(mongoDB Node.js Driverのドキュメント)。このオブジェクトから格納しているデータを取り出すには、readメソッドを使用する。オブジェクトのbufferプロパティを直接参照することでもバイナリデータにアクセスできるのだが、ドキュメントにはこのプロパティが明示されていないので、readメソッドを使用したほうが確実だ。

binary.read(position, length)

 ここでposition引数にはデータの読み出しを開始するオフセットを、length引数には読み出すバイト数を指定する。

 次の例は、読み出したデータをファイルに保存するものだ。

 データの取り出し処理をしているのは60〜61行の部分だ。まずlengthメソッドでバイナリのサイズを取得してlen変数に格納し、0バイト目からlen変数で指定したサイズまで、つまりバイナリの最初から最後までを取り出してコールバック関数を実行している。

Node.jsのhttp.request関数/http.get関数とタイムアウト

posted on 2013/04/08 01:18:39

 Node.jsのhttpモジュールには、HTTPクライアント機能も実装されている。http.request関数およびhttp.get関数がそれだ。詳細はドキュメントを確認していただきたいが、http.requestは任意のリクエストメソッドを使ってリクエストを送信できるものだ。また、http.getはGETリクエストに限定されるものの、URLを与えるだけで簡単にリクエストを送信できる。

 たとえば以下のコード(http-request.js)は、引数で指定したURLに対しGETリクエストを送信し、取得したコンテンツを表示するものだ。

http-request.js
#!/usr/local/bin/node
var http = require('http');

// check arguments
if (process.argv.length < 3) {
  process.exit(-1);
}
var targetUrl = process.argv[2]

// send request
var req = http.get(targetUrl, function(res) {
  // output response body
  res.setEncoding('utf8');
  res.on('data', function(str) {
    console.log(str);
  });
});

// error handler
req.on('error', function(err) {
  console.log("Error: " + err.message);
});

 http.request関数やhttp.get関数はともに戻り値としてhttp.ClientRequest型のrequestオブジェクトを返す。http.request関数の場合、request.endメソッドを実行して明示的にリクエストの送信を完了させる必要があるが、http.get関数の場合はendメソッドは暗黙的に実行されるので不要だ。

 http-request.jsを以下のように実行すると、http://hylom.net/のコンテンツがコンソールに表示される。

$ node http-request.js http://hylom.net/

 さて、http.get関数やhttp.request関数を使ってリクエストを送信する場合、デフォルトではタイムアウト時間が設定されない。つまり、サーバーへの接続が成功した後、サーバー側がレスポンスの送信を完了しない限りクライアント側の処理は終了しない。

 たとえば、次のslowserver.jsはローカルホストの4000番ポートで待ち受けをし、リクエストを受信してから5分(300秒)後にレスポンスを返すというものだ。

slowserver.js
#!/usr/local/bin/node
var http = require('http');

server = http.createServer();
server.on('request', function(req, res) {
  setTimeout(function() {
    res.setHeader('Content-Type', 'text/plain');
    res.writeHead(200);
    res.write('200 - OK.');
    res.end();
  }, 300 * 1000);
});

server.listen(4000, function() {
  console.log('start listening on port 4000...');
});

 このサーバーを実行し、先ほどのhttp-request.jsを使ってアクセスすると、実行後5分をすぎたあたりでレスポンスが表示される。

$ node slowserver.js &
start listening on port 4000...

$ node http-request.js http://localhost:4000/
200 - OK.
$ fg
node slowserver.js
^C

 さて、タイムアウトが設定されていないということは、サーバー側がレスポンスを返さない、もしくは接続を破棄しない限り、クライアント側は永遠にレスポンスを待ち続けることになる。クライアント側でタイムアウト時間を設定するには、http.get関数やhttp.request関数の戻り値として返されるhttp.ClientRequestクラスのsetTimeoutメソッドを利用する。

request.setTimeout(timeout, [callback])

 timeout引数にはタイムアウト時間を指定する。単位はミリ秒だ。接続後、ここで指定された時間が経過するとrequestオブジェクトに'timeout'イベントが発生する。callback引数はtimeoutイベント発生時に一度だけ実行されるコールバック関数を指定する。なお、'timeout'イベントハンドラは引数を取らない。

 ここで注意したいのが、timeoutイベントが発生した場合でも接続は継続されたままになっている点だ。次のコードは、先のhttp-request.jsにrequest.setTimeoutメソッドを使ってタイムアウトを設定したものだ。

http-request.js(改造版その1)
#!/usr/local/bin/node

var http = require('http');

// check arguments
if (process.argv.length < 3) {
  process.exit(-1);
}
var targetUrl = process.argv[2]

var req = http.get(targetUrl, function(res) {
  console.log('get response')
  res.setEncoding('utf8');
  res.on('data', function(str) {
    console.log(str);
  });
});
req.setTimeout(1000);

req.on('timeout', function() {
  console.log('request timed out');
  req.abort()
});

req.on('error', function(err) {
  console.log("Error: " + err.code + ", " + err.message);
});

 これを実行すると、次のようにtimeoutイベントは発生しているものの、イベント発生後も引き続きレスポンスを待ち続けていることが分かる。

request timed out
200 - OK.

 timeoutイベント発生時に接続を破棄したい場合、イベントハンドラ内でrequest.abortメソッドなどを使用して明示的に接続を破棄する操作を行えばよい。

req.on('timeout', function() {
  console.log('request timed out');
  req.abort()
});

 このように修正した上で先ほどのhttp-request.jsを実行すると、次のようにタイムアウト発生時に接続が破棄され、errorイベントが発生する。

$ node http-request.js http://localhost:4000/
request timed out
Error: ECONNRESET, socket hang up

Connectミドルウェア「logger」でExpressアプリケーションのアクセスログ/エラーログを記録する

posted on 2013/03/25 02:03:46

 Expressでは、Connectミドルウェア「logger」を使うことで詳細なアクセスログやエラーログを記録することができる。Expressコマンドで生成したスケルトンコードでは、以下のようにloggerミドルウェアを利用するように設定されている。

app.configure(function(){
  :
  :
  app.use(express.logger('dev'));
  :
  :
}

 このように第一引数に'dev'引数を与えて生成されたloggerミドルウェアを使用すると、標準出力にカラー付きでアクセスログが出力される。これは開発用という目的なので、実サービスの運用には適していない。そこで、Apacheのデフォルト設定で使われる形式のアクセスログおよびエラーログをファイルに出力するよう設定していく。

 なお、下記ではExpress 3.1.0およびこれが依存しているConnect 2.7.2ベースを使用している。

logger型オブジェクトを生成する

 loggerミドルウェアオブジェクトのファクトリ関数は、以下のような引数を取る。

logger([options])

 options引数にはオプション情報を格納したオブジェクト、文字列、関数オブジェクトを与えることができる。省略された場合は空のオブジェクト({})が与えられたものと見なされる。また、文字列もしくは関数オブジェクトが与えられた場合、次のようなオブジェクトが与えられたものと見なされる。

{format: <与えられた文字列もしくは関数オブジェクト>}

 options引数に与えられたオブジェクトのうち、表1のプロパティが挙動に影響を与える。

表1 options引数で有効なプロパティ
プロパティ名説明デフォルト値
immediate非falseの値を指定すると、リクエストを受信した時点でログ出力を実行する。false、もしくはundefinedの場合、レスポンスの送信が完了した時点(responseオブジェクトのendイベントハンドラが呼び出されるタイミング)でログ出力を実行するundefined
format出力フォーマットを指定する。表2のどれかに該当する文字列が指定された場合、それに対応する定義済みフォーマットが使用される。それ以外の文字列が指定された場合、その文字列をフォーマット文字列として使用する'default'
streamログを出力するストリームを指定するprocess.stdout
bufferバッファを利用する場合、非falseの値を指定する。数値が指定された場合、フラッシュする間隔をミリ秒で指定するundefined

 また、options.formatプロパティでは出力形式を指定できるが、ここでは表2の定義済みフォーマット値が利用できる。「tiny」と「dev」はほぼ同じ内容を出力するが、devはコンソール出力時に見やすいよう色付き表示のためのエスケープシーケンス付きで出力される点が異なる。

表2 使用できる定義済みフォーマット
定義済みフォーマット名フォーマット文字列
default:remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
short:remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms
tiny:method :url :status :res[content-length] - :response-time ms
dev:method :url :status :response-time ms - :res[content-length]

 なお、フォーマット文字列では表3のトークンが利用できる。これらは出力時に適切な値に変換される。

表3 フォーマット文字列で使用できるトークン
トークン名変換後の文字列
:urlreq.originalUrl || req.url
:methodreq.method
:response-timenew Date - req._startTime
:datenew Date().toUTCString()
:statusres.statusCode
:referrerreq.headers['referer'] || req.headers['referrer']
:remote-addrreq.ipもしくはsock.socket.remoteAddress、sock.remoteAddress
:http-versionreq.httpVersionMajor + '.' + req.httpVersionMinor
:user-agentreq.headers['user-agent']
:req[<リクエストヘッダ名>]指定したリクエストヘッダの値
:res[<レスポンスヘッダ名>]指定したレスポンスヘッダの値

 定義済みフォーマットで所望の出力が得られない場合、表3のフォーマット文字列を使って独自のフォーマットを定義することも可能だ。さらに、options.formatプロパティに独自のログ整形処理を実装した関数オブジェクトを与えることもできる。この場合、この関数オブジェクトはログを出力するタイミングで(exports, req, res)という引数が与えられて実行される。exports引数にはloggerモジュールが、req引数にはリクエストオブジェクトが、res引数にはレスポンスオブジェクトが与えられ、この関数の戻り値がログとして指定されたストリームに出力される。

Apacheのデフォルト設定で使われる形式のログを出力する

 以下はApacheのデフォルト設定で出力されるログの例だ。

192.168.0.1 - - [24/Mar/2013:06:07:06 +0900] "GET /feed/ HTTP/1.1" 304 - "-" "hogehoge user-agent"

 このとき、各フィールドには次のような情報が格納されている。なお、定義されていない/取得できない値に対応するフィールドには「-」が出力される。

<リモートホスト名> <identd(mod_ident)が提供するリモートログ名> <リモートユーザー名> <アクセス日時> "<HTTPリクエストヘッダ>" <ステータスコード> <転送バイト数> "<Refererリクエストヘッダの値>" "<User-Agentリクエストヘッダの値>"

 これと同じ形式のログをloggerミドルウェアを使って出力するには、定義済みフォーマットの「default」を利用すればよい。たとえば/home/foobar/access_logというファイルにログを出力するには、以下のようにする。

app.configure(function(){
  :
  :
  var logStream = fs.createWriteStream('/home/foobar/access_log', {mode: 'a'});
  app.use(express.logger({
    format: 'default',
    stream: logStream || process.stdout
    }));
  :
  :
}

プログラミングにおいて重要な知識をどこから学んだか

posted on 2012/10/17 16:34:25

 「大切な事は全て.NETから学んだ」を見て、ああそういう人もいるんだなと思ったので自分がプログラミング関連知識をどこから学んだかを挙げてみる。

  • プログラミングとは何か:BASIC。
  • コンピュータのアーキテクチャ:アセンブラ(とFM-7のマニュアル)。
  • 基本的な2D画像作成:BASICとアセンブラ
  • 「関数」という仕組みとスコープ:C
  • オブジェクト指向、クラス、継承:C++
  • マルチスレッド、同期/排他処理:C/C++
  • MVCアーキテクチャ:MFC
  • イテレータ、連想配列:STL
  • ポリゴンやそれを使った3Dレンダリング:OpenGL
  • 関数型言語:emacs lisp
  • 正規表現:Perl
  • ラムダ関数:大学院のプログラミング理論の授業
  • モジュールリポジトリの利用:Perl(CPAN)
  • 「読みやすい」コードの書き方:Perl(というか書籍「Perlベストプラクティス」)
  • Webアプリケーションフレームワーク:Perl(Catalyst)
  • テンプレートエンジン:Perl(Template Toolkit)
  • メッセージ型アーキテクチャ:Objective-C
  • ダックタイピング、動的型付け、リスト内包:Python
  • 並列処理:OpenMP
  • プロトタイプベースのクラス機構、クロージャ:JavaScript

 見事にJavaとPHPとRubyからは何も学んでいない、というかこれらの言語は読めるけどあまり書けない(汗)。

SDL_mixerのspecファイル

posted on 2012/09/12 16:54:03

 SDL_mixerはCentOS用のパッケージが用意されていない&提供されているSRPMをx64環境でビルドすると64ビットバイナリが/usr/lib/以下にインストールされてしまうという問題があるので、これを修正するspecファイルを作成した。Gist:SDL_mixer.specで公開している。