前回の記事ではダウンロードをひと通り取り扱った。
今回はデータをアップロードする処理を実装してみることにしよう。
前回記事↓
[iOS] NSURLSessionを使って通信を行う

いきなりマルチパートデータをPOSTするのではなく、簡単なところから少しずつステップアップしていこう。
なお、今回の記事を作成するにあたり、こちらを大いに参考にさせていただいた。

Send POST request using NSURLSession - stackoverflow

テキストデータをPOSTする

まずは一番簡単な処理、テキストのみのPOSTだ。
このように実装する。
基本的な流れはダウンロードの時とほとんど変わらない。
ほんの少しだけ処理を付け加えてやればいいだけだ。
    NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
これはHTTPリクエストの設定をするためのものだ。
具体的な設定方法については後で出てくる。
    NSData* data = [@"param1=りんご&param2=みかん" dataUsingEncoding:NSUTF8StringEncoding];
ここは特に重要な箇所だ。
POSTするデータはテキストのみにしろ、画像を加えるにしろ、全てNSDataにすることになる。
今回はテキストのみなので、作成は実にシンプルだ。
"りんご"と"みかん"という2つのテキストをPOSTする。 
    request.HTTPMethod = @"POST";
    request.HTTPBody = data;
ここで先ほど作成したリクエストの設定を行っている。
MethodとBodyを代入してやれば設定は完了だ。

後はダウンロードの時と同じ流れでタスクを作成し、実行させてやればいい。

テキストと画像をマルチパートデータにしてPOSTする

いよいよ今回の本題だ。
テキストと画像をマルチパートデータにしてPOSTしてみよう。
このように実装する。
こんな長い処理になってしまってうんざりしてしまうかもしれない。
しかし、データの組み立て部分以外はテキストのみの場合とほとんど同じで、違うのはこの部分だ。
    NSString* boundary = @"MyBoundaryString";
    NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.HTTPAdditionalHeaders =
    @{
      @"Content-Type" : [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]
     };
リクエストヘッダにContent-Typeを追加し、マルチパートであることを明示する。
boundaryというのはマルチパートでデータの境界を表すためのもので、ここでは単語として読めるものにしてあるが、実際はデータと被らないようなランダムな文字列にした方が良いだろう。

次はいよいよ今回の肝、マルチパートデータの組み立てだ。
    NSMutableData* data = [NSMutableData data];
    
    // テキスト部分の設定
    [data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data;"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"name=\"%@\"\r\n\r\n", @"param1"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"%@\r\n", @"すいか"] dataUsingEncoding:NSUTF8StringEncoding]];
    
    // 画像の設定
    [data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data;"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"name=\"%@\";", @"upload_file"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"filename=\"%@\"\r\n", @"sample1.jpg"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:[[NSString stringWithFormat:@"Content-Type: image/jpeg\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [data appendData:imageData];
    [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    
    // 最後にバウンダリを付ける
    [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
一見するとかなり複雑そうに見えるが、ある程度の規則性があるのはわかる。
この部分の説明を行うととても長くなってしまうので、おまじない的にこうすれば良いくらいに捉えてても構わない。
どうしても詳細を知りたい場合はRFCを読んでみるなりしてほしい。

なお、一つだけ気をつけなければならない点がある。

Objective-Cでは(Xcodeでは?)'\'は'¥'とは異なるということだ。
'¥'でダブルクォーテーションをエスケープしようとするとXcode上でエラーが出るから気づくとは思うが、ここはハマりやすいところなので注意しよう。

任意の数のテキストと画像をPOSTするメソッドを作成する

最後に総まとめとして、任意の数のテキストと画像をPOSTするメソッドを作成する。
NSDictionaryにデータを設定し、引数として渡すと、それらをまとめてPOSTするというものだ。

このように実装する。

- (void)postMultiDataWithTextDictionary:(NSDictionary*)textDictionary
                           imageDictionary:(NSDictionary*)imageDictionary
                                url:(NSURL*)url
                           delegate:(id<NSURLSessionDelegate>)delegate
{
    NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSString* boundary = @"MyBoundaryString";
    config.HTTPAdditionalHeaders =
    @{
      @"Content-Type" : [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]
      };
    NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
    NSURLSession* session = [NSURLSession sessionWithConfiguration:config
                                                          delegate:delegate
                                                     delegateQueue:[NSOperationQueue mainQueue]];
    
    // postデータの作成
    NSMutableData* data = [NSMutableData data];
    
    // テキスト部分の設定
    for (id key in [textDictionary keyEnumerator])
    {
        NSString* value = [textDictionary valueForKey:key];
        
        [data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data;"] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"%@\r\n", value] dataUsingEncoding:NSUTF8StringEncoding]];
    }
    
    // 画像の設定
    for (int i = 0; i < [imageDictionary count]; i++)
    {
        NSString* key = [[imageDictionary allKeys] objectAtIndex:i];
        NSData* value = [imageDictionary valueForKey:key];
        NSString* name = [NSString stringWithFormat:@"upload_file%d", i];
        
        [data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data;"] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"name=\"%@\";", name] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"filename=\"%@\"\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:[[NSString stringWithFormat:@"Content-Type: image/jpeg\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
        [data appendData:value];
        [data appendData:[[NSString stringWithFormat:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    }
    
    // 最後にバウンダリを付ける
    [data appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    request.HTTPMethod = @"POST";
    request.HTTPBody = data;
    NSURLSessionDataTask* task = [session dataTaskWithRequest:request];
    
    [task resume];
}
これを使用するにはこのように呼び出す。
なお、NSURLSessionではメモリリークが起きる場合があるので気をつけたほうが良い。
詳しくはこちらの記事にまとめた。

[iOS] NSURLSessionのメモリリークに気をつける