読み込み中...

Objective-CのcompletionHandlerの使い方と応用例10選

Objective-CのcompletionHandlerの詳しい使い方 Objctive-C
この記事は約29分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Objective-CはAppleのiOSやmacOSを中心に使用されるプログラミング言語のひとつです。

Objective-Cの多くの機能やメソッドの中でも、非常に便利で強力なものとして「completionHandler」があります。

この記事では、Objective-CにおけるcompletionHandlerの使い方から応用例まで、初心者向けにわかりやすく10のサンプルコードとともに解説します。

●Objective-Cとは

Objective-Cは、C言語をベースにしてオブジェクト指向の概念を取り入れたプログラミング言語です。

AppleのiOSやmacOSアプリケーションの開発において、長らく主要な言語として使用されてきました。

近年ではSwiftが主流となってきていますが、Objective-Cで書かれた既存のコードやライブラリを利用する場面も少なくありません。

○Objective-Cの基本的な特徴

  1. C言語との互換性:Objective-CはC言語のスーパーセットであり、C言語のコードをそのままObjective-Cの中で動かすことができます。
  2. 動的なオブジェクト指向:オブジェクト指向の概念が強化されており、動的なメソッドの解決が特徴です。これにより、ランタイム中にメソッドの振る舞いを変更することも可能となります。
  3. メッセージパッシング:Objective-Cでは、オブジェクト間のコミュニケーションはメッセージパッシングという形で行います。これにより、メソッド呼び出し時の柔軟性が向上します。
  4. 豊富なライブラリ:CocoaやCocoa Touchなどのフレームワークが提供されており、これらを利用することで高機能なアプリケーションを効率的に開発することができます。

Objective-Cを使用したプログラミングには多くのメリットがありますが、その中でもcompletionHandlerは非常に注目される機能の一つです。

非同期処理やコールバック処理をスムーズに行うことができ、アプリケーションの応答性やパフォーマンスの向上に貢献します。

●completionHandlerとは

completionHandlerは、Objective-Cの中で非同期処理やコールバックを管理するためのツールとして使用されます。

特定のタスクが完了した後に実行されるブロック(関数やメソッドの一種)を指定することができます。

簡単に言うと、何かの処理が終了した後に、「何をするか」を決定するためのツールとして使います。

○completionHandlerの概要

Objective-Cでのプログラミング作業中、多くのタスクが非同期で行われる場面があります。

例えば、ネットワークリクエストやデータベースのクエリなどです。

これらの非同期タスクが完了したときに特定のコードを実行したい場面が多々あります。

completionHandlerはこのような場面で非常に役立ちます。

具体的には、completionHandlerはメソッドや関数の引数として渡されるブロックです。

このブロックは、非同期タスクの終了時に自動的に呼び出されます。

ブロックの内容は、開発者が任意で定義できるため、非常に柔軟な処理が可能です。

○なぜcompletionHandlerを使うのか

completionHandlerを使用する主な理由は、非同期タスクの完了後に特定の処理を確実に実行するためです。

非同期処理は、主要なスレッド(メインスレッドなど)をブロックせずにバックグラウンドでタスクを実行することができます。

このため、アプリケーションのユーザビリティが向上します。

しかし、非同期タスクが完了した後に何らかの処理を行う必要がある場合、そのタスクの終了を確実に検知して、適切なタイミングで処理を実行する必要があります。

例として、データベースからデータを非同期に取得するタスクを考えてみましょう。

このデータ取得タスクが完了した後に、取得したデータを画面に表示する処理を行いたい場合、completionHandlerを使用することで、データ取得が完了したタイミングを正確に検知し、画面表示の処理を行うことができます。

●completionHandlerの基本的な使い方

Objective-CにおけるcompletionHandlerの概念は、非同期処理や後続のタスクの完了を検知して処理を実行する際に使用されます。

非同期処理は、メインスレッドとは別のスレッドで実行されるタスクを指し、このタスクの完了タイミングを正確に把握することは難しいです。

completionHandlerは、この非同期タスクが完了した後に実行されるブロック(処理のかたまり)を指定することができます。

○サンプルコード1:基本的なcompletionHandlerの使用方法

Objective-CにおけるcompletionHandlerの基本的な使用方法を紹介します。

// 非同期に実行されるタスクを示すメソッド
- (void)asyncTaskWithCompletion:(void (^)(NSString *result))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 何らかの非同期処理を行う
        [NSThread sleepForTimeInterval:2.0];  // 2秒待機を示す

        // 非同期処理が完了したらcompletionHandlerを呼び出す
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(@"非同期処理完了!");
            }
        });
    });
}

このコードでは、非同期タスクを表すメソッド内で、2秒間の待機処理を行ってからcompletionHandlerを呼び出しています。

completionHandlerの中で指定された処理(ブロック)が非同期タスクの完了後に実行されます。

この例では、非同期に実行されるタスクを表すメソッドを定義し、その後にタスクが完了した際の処理をcompletionHandlerとして指定しています。

次に、このメソッドを呼び出す際のコードを見てみましょう。

[self asyncTaskWithCompletion:^(NSString *result) {
    NSLog(@"%@", result);
}];

このコードを実行すると、2秒後にコンソールに「非同期処理完了!」と表示されます。

completionHandlerを使って、非同期処理が完了したことを検知し、その後の処理を行うことができました。

○サンプルコード2:completionHandlerを持つメソッドの作成

次に、独自のメソッド内でcompletionHandlerを持つ方法を見ていきます。

- (void)fetchDataWithCompletion:(void (^)(NSArray *data, NSError *error))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // データを取得する非同期処理を示す
        [NSThread sleepForTimeInterval:2.0];  // 2秒待機

        NSArray *dataFetched = @[@"data1", @"data2", @"data3"];

        // 非同期処理が完了したらcompletionHandlerを呼び出す
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(dataFetched, nil);
            }
        });
    });
}

このコードでは、データを取得する非同期のタスクを表すメソッド内で、2秒間の待機処理を行った後、取得したデータをcompletionHandler経由で返します。

この例では、非同期にデータを取得するメソッドを定義し、その後に取得したデータをcompletionHandlerとして返しています。

このメソッドを呼び出す際のコードは次のようになります。

[self fetchDataWithCompletion:^(NSArray *data, NSError *error) {
    if (data) {
        NSLog(@"取得したデータ: %@", data);
    } else if (error) {
        NSLog(@"エラー: %@", error.localizedDescription);
    }
}];

このコードを実行すると、2秒後にコンソールに「取得したデータ: (data1, data2, data3)」と表示されます。

completionHandlerを使って、非同期処理で取得したデータを取得し、その後の処理を行うことができました。

●completionHandlerの応用例

Objective-Cのプログラミングにおいて、completionHandlerは非常に便利なツールとなっています。

初心者から経験者まで、多くの開発者がcompletionHandlerの力を利用して、効率的なプログラミングを実現しています。

ここでは、具体的な応用例とともに、その使い方を解説します。

○サンプルコード3:非同期処理の結果を取得する

非同期処理を行う際、処理が完了した時点で何らかのアクションを行いたい場面は多いです。

このコードでは、非同期処理後にcompletionHandlerを使って結果を取得する方法を表しています。

- (void)fetchDataWithCompletion:(void (^)(NSString *data, NSError *error))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // データの取得処理(ダミー)
        NSString *data = @"取得データ";
        // 処理完了後のコールバック
        dispatch_async(dispatch_get_main_queue(), ^{
            if (data) {
                completion(data, nil);
            } else {
                NSError *error = [NSError errorWithDomain:@"ErrorDomain" code:0 userInfo:nil];
                completion(nil, error);
            }
        });
    });
}

このコードでは非同期でデータを取得し、取得完了後にmain queueでcompletionHandlerを呼び出しています。

この例ではデータを正常に取得した場合はデータを返し、エラーが発生した場合はエラー情報を返しています。

このコードの結果として、非同期処理が完了した後に、データの取得が成功していれば取得したデータを返し、エラーが発生していればエラー情報を返すという動作を期待することができます。

○サンプルコード4:多数の非同期タスクを順番に処理する

非同期処理を多数行う場面で、順番に処理を行いたい場面があります。

このコードでは、completionHandlerを使って多数の非同期タスクを順番に実行する方法を表しています。

- (void)performTasksSequentiallyWithCompletion:(void (^)(NSArray *results, NSError *error))completion {
    NSMutableArray *results = [NSMutableArray array];

    [self firstTaskWithCompletion:^(NSString *result, NSError *error) {
        if (error) {
            completion(nil, error);
            return;
        }
        [results addObject:result];

        [self secondTaskWithCompletion:^(NSString *result, NSError *error) {
            if (error) {
                completion(nil, error);
                return;
            }
            [results addObject:result];

            // さらにタスクが続く場合、同様の形式で追加

            completion(results, nil);
        }];
    }];
}

このコードでは、firstTaskWithCompletionが完了した後にsecondTaskWithCompletionが実行されるようになっています。

これを繰り返して、すべてのタスクが完了した際に、一度にすべての結果をcompletionHandlerに返しています。

このコードの結果として、非同期タスクが順番に実行され、すべてのタスクが完了したら結果の配列が返される動作を期待することができます。

○サンプルコード5:UIの更新をcompletionHandlerで行う

Objective-Cにおいて、非同期処理の後にUIを更新する際には、completionHandlerを活用すると非常に便利です。

UIの更新は主にメインスレッドで行われるため、completionHandlerを使って非同期処理が完了した時点で、メインスレッド上でUIの更新を行うことができます。

このコードでは、非同期処理を行い、その結果をcompletionHandlerを介して取得してUIのラベルを更新するコードを表しています。

この例では、非同期処理であるfetchData関数を呼び出し、取得したデータをラベルに表示しています。

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UILabel *dataLabel;

- (void)fetchDataWithCompletion:(void (^)(NSString *result))completion;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self fetchDataWithCompletion:^(NSString *result) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.dataLabel.text = result;
        });
    }];
}

- (void)fetchDataWithCompletion:(void (^)(NSString *result))completion {
    // 非同期処理(例: ネットワークリクエスト)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // データ取得の処理(ここでは仮のデータを設定)
        NSString *data = @"取得したデータ";
        completion(data);
    });
}

@end

このコードを実行すると、非同期でデータを取得した後、dataLabelのテキストが「取得したデータ」として更新されます。

このように、completionHandlerを活用することで、非同期処理の結果をUIに反映させることが手軽にできます。

○サンプルコード6:外部APIからのデータ取得後の処理

外部APIを利用してデータを取得する場面も多くあります。

このような場合、completionHandlerを使用することで、APIからのデータ取得が完了した時点での処理を簡単に実装することができます。

このコードでは、外部APIからデータを取得し、その結果をcompletionHandlerで受け取るコードを表しています。

この例では、外部APIからデータを取得し、そのデータを利用して何らかの処理を行っています。

#import <UIKit/UIKit.h>

@interface APIViewController : UIViewController

- (void)fetchAPIWithCompletion:(void (^)(NSDictionary *data, NSError *error))completion;

@end

@implementation APIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self fetchAPIWithCompletion:^(NSDictionary *data, NSError *error) {
        if (error) {
            NSLog(@"エラー: %@", error.localizedDescription);
            return;
        }

        // データを利用した処理
        NSLog(@"取得したデータ: %@", data);
    }];
}

- (void)fetchAPIWithCompletion:(void (^)(NSDictionary *data, NSError *error))completion {
    // APIリクエストの処理
    NSURL *url = [NSURL URLWithString:@"https://api.example.com/data"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            completion(nil, error);
            return;
        }

        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        completion(json, nil);
    }] resume];
}

@end

このコードを実行すると、APIからデータを非同期で取得し、そのデータをログに表示します。

また、エラーが発生した場合は、エラー情報をログに出力します。

このように、completionHandlerを使用することで、外部APIのレスポンスを効果的にハンドルすることができます。

○サンプルコード7:ユーザー入力後の処理をcompletionHandlerで行う

このコードでは、ユーザーからの入力を受け取った後、completionHandlerを使ってその結果に応じた処理を行う例を表しています。

この例では、ユーザーに名前を入力してもらい、その名前が入力されたらメッセージを返すという処理を行っています。

#import <Foundation/Foundation.h>

void getInputFromUserWithCompletion(void (^completion)(NSString *input)) {
    char buffer[256];
    NSLog(@"あなたの名前を入力してください: ");
    fgets(buffer, sizeof(buffer), stdin);
    NSString *inputString = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
    completion(inputString);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        getInputFromUserWithCompletion(^(NSString *input) {
            NSLog(@"こんにちは、%@さん!", input);
        });
    }
    return 0;
}

このコードのポイントはgetInputFromUserWithCompletion関数です。

この関数では、引数としてcompletionHandlerを取り、その中でユーザーの入力を待ち、入力が完了したらcompletionHandlerを呼び出しています。

このコードを実行すると、ユーザーに名前の入力を求められ、名前を入力すると「こんにちは、[入力した名前]さん!」というメッセージが表示されることが期待されます。

○サンプルコード8:completionHandlerを持つ自作関数の例

このコードでは、自作の関数内でcompletionHandlerを持つ関数を作成し、その結果に応じた処理を行う例を表しています。

この例では、数値を2倍にする関数を作成し、その結果をcompletionHandlerを使って返すというシンプルな内容となっています。

#import <Foundation/Foundation.h>

void doubleTheNumber(int number, void (^completion)(int result)) {
    int doubledNumber = number * 2;
    completion(doubledNumber);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int originalNumber = 5;
        doubleTheNumber(originalNumber, ^(int result) {
            NSLog(@"%dの2倍は%dです。", originalNumber, result);
        });
    }
    return 0;
}

このコードの主要部分はdoubleTheNumber関数です。

この関数では、与えられた数値を2倍にし、その結果をcompletionHandlerで返しています。

このコードを実行すると、「5の2倍は10です。」というメッセージが表示されることが期待されます。

○サンプルコード9:エラーハンドリングを伴う例

Objective-CでのcompletionHandlerを使用した非同期処理は、エラーハンドリングが非常に重要です。

非同期処理の結果として、エラーが発生することも考えられます。

そのため、エラーハンドリングを正しく行うことで、アプリケーションの安定性を保つことができます。

ここでは、エラーハンドリングを伴ったcompletionHandlerの使用例を紹介します。

// 非同期処理を行う関数の定義
- (void)asyncTaskWithCompletion:(void (^)(NSString *result, NSError *error))completion {
    // 非同期処理を模倣
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 処理結果としての文字列
        NSString *result = @"成功しました";

        // ここではエラーを模倣しないが、エラーが発生した場合は以下のようにエラーオブジェクトを作成
        // NSError *error = [NSError errorWithDomain:@"com.example" code:1001 userInfo:nil];

        // completionHandlerを呼び出し
        if (completion) {
            completion(result, nil);
        }
    });
}

// 使用例
[self asyncTaskWithCompletion:^(NSString *result, NSError *error) {
    if (error) {
        NSLog(@"エラーが発生しました: %@", error);
    } else {
        NSLog(@"処理結果: %@", result);
    }
}];

このコードでは、非同期タスクを実行し、その結果をcompletionHandlerを使って取得する関数を定義しています。

この例では、非同期処理の結果としてエラーが発生することを模倣していませんが、エラーが発生した場合はNSErrorオブジェクトを生成し、completionHandlerに渡しています。

エラーがない場合は、処理の結果としての文字列をcompletionHandlerに渡します。

このコードを実行すると、「処理結果: 成功しました」と表示されます。

もしエラーを模倣した場合は、「エラーが発生しました: [エラー内容]」というように表示されるでしょう。

○サンプルコード10:completionHandlerと他の関数との連携例

completionHandlerは、非同期処理の結果を外部の関数に渡すためにも使用できます。

このようにして、非同期処理の結果を基に別の関数を実行することができます。

ここでは、completionHandlerを使用して非同期処理の結果を別の関数に渡す例を紹介します。

// 非同期処理を行う関数の定義
- (void)fetchDataWithCompletion:(void (^)(NSArray *data))completion {
    // 非同期処理を模倣
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *fetchedData = @[@"データ1", @"データ2", @"データ3"];
        if (completion) {
            completion(fetchedData);
        }
    });
}

// データを処理する関数
- (void)processData:(NSArray *)data {
    for (NSString *item in data) {
        NSLog(@"データの処理: %@", item);
    }
}

// 使用例
[self fetchDataWithCompletion:^(NSArray *data) {
    [self processData:data];
}];

このコードでは、非同期にデータを取得する関数と、そのデータを処理する関数の2つを定義しています。

非同期にデータを取得する関数は、取得したデータをcompletionHandlerを通して渡し、それを基にデータ処理関数を実行します。

このコードを実行すると、次のように表示されます。

データの処理: データ1
データの処理: データ2
データの処理: データ3

●completionHandlerを使用する際の注意点と対処法

Objective-CのcompletionHandlerは非常に便利な機能で、非同期処理の終了後に特定の処理を実行する場面で頻繁に使用されます。

しかし、その便利さゆえに、うまく扱わないと意図しない動作やエラーの原因となることも。

ここでは、completionHandlerを使用する際の一般的な注意点と、それに対する対処法を解説していきます。

○循環参照の問題

completionHandlerの中でselfや他のオブジェクトを強参照すると、循環参照が発生することがあります。

これにより、オブジェクトが適切に解放されず、メモリリークを引き起こす可能性が高まります。

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation MyClass
- (void)doSomething {
    [self someMethodWithCompletion:^{
        self.name = @"NewName";  // selfを強参照
    }];
}
@end

このコードでは、completionHandlerの中でselfを強参照しています。

この例では、MyClassインスタンスが解放されることなく残り続ける可能性があります。

対処法として、循環参照を避けるためには、__weakキーワードを使用してselfを弱参照するのが一般的です。

@implementation MyClass
- (void)doSomething {
    __weak typeof(self) weakSelf = self;
    [self someMethodWithCompletion:^{
        weakSelf.name = @"NewName";
    }];
}
@end

このように、weakSelfを介して参照することで、循環参照の問題を回避できます。

○メインスレッドでのUI更新

completionHandlerの中でUIを更新する場合、その更新は必ずメインスレッドで行う必要があります。

バックグラウンドスレッドでUIを直接更新すると、アプリがクラッシュするリスクがあります。

[self fetchDataWithCompletion:^(NSData *data) {
    UIImage *image = [UIImage imageWithData:data];
    self.imageView.image = image; // これはメインスレッドで実行する必要がある
}];

上記のコードでは、データをフェッチした後に画像をUIにセットしています。

このようなUIの更新は、メインスレッドで行う必要があります。

対処として、dispatch_asyncを使用してメインスレッドでUIの更新を行います。

[self fetchDataWithCompletion:^(NSData *data) {
    dispatch_async(dispatch_get_main_queue(), ^{
        UIImage *image = [UIImage imageWithData:data];
        self.imageView.image = image;
    });
}];

この方法を使用することで、UIの更新は確実にメインスレッド上で実行され、アプリの安全性を保つことができます。

●Objective-CのcompletionHandlerのカスタマイズ方法

Objective-CのcompletionHandlerは、特定のタスクが完了したときに実行されるブロックのことを指します。

このcompletionHandlerは非常に強力で、様々なタスクの後に特定の処理を実行する際に使用されます。

しかし、標準のcompletionHandlerだけでは、特定のニーズに応えることが難しい場面も存在します。

そのため、completionHandlerをカスタマイズする方法について説明します。

completionHandlerをカスタマイズすることで、より柔軟に非同期処理や後続のタスクを制御することができます。

例えば、特定の条件下で異なる処理を実行したい、複数のcompletionHandlerを組み合わせたい、エラーハンドリングを追加したいなどのニーズに応えることができます。

○パラメータの追加

このコードでは、completionHandlerにパラメータを追加して、非同期処理の結果に基づいて異なる処理を実行する方法を表しています。

この例では、成功した場合と失敗した場合の処理を分岐しています。

typedef void (^CustomCompletionHandler)(BOOL success, NSError *error);

- (void)fetchDataWithCompletion:(CustomCompletionHandler)completion {
    // データ取得の処理
    // ...

    if (/* 成功の条件 */) {
        completion(YES, nil);
    } else {
        NSError *error = /* エラーの生成 */;
        completion(NO, error);
    }
}

このコードでは、CustomCompletionHandlerという独自のcompletionHandlerを定義しています。

この例では、BOOL型のsuccessとNSError型のerrorをパラメータとして受け取ります。

データの取得が成功した場合、successYESを、失敗した場合にはNOとともにエラー情報をerrorに渡します。

○複数のcompletionHandlerの組み合わせ

ある非同期タスクの終了後に、別の非同期タスクを開始し、その結果に基づいてさらに別のタスクを実行するような複雑な処理フローを持つ場合、複数のcompletionHandlerを組み合わせることで実現することができます。

typedef void (^FirstCompletion)(BOOL success);
typedef void (^SecondCompletion)(NSString *result);

- (void)firstTaskWithCompletion:(FirstCompletion)firstCompletion {
    // 最初のタスクの処理
    // ...

    if (/* 成功 */) {
        firstCompletion(YES);
    } else {
        firstCompletion(NO);
    }
}

- (void)secondTaskWithCompletion:(SecondCompletion)secondCompletion {
    [self firstTaskWithCompletion:^(BOOL success) {
        if (success) {
            // 第二のタスクの処理
            NSString *result = /* 処理結果 */;
            secondCompletion(result);
        } else {
            secondCompletion(nil);
        }
    }];
}

このコードでは、最初の非同期タスクの終了後に、第二の非同期タスクを開始しています。

第一のタスクが成功した場合のみ、第二のタスクを実行し、その結果を返します。

まとめ

Objective-CのcompletionHandlerは非同期処理や後続タスクの制御に非常に役立つツールです。

基本的な使用方法だけでなく、カスタマイズによって様々なニーズに対応することができます。

パラメータの追加や複数のcompletionHandlerの組み合わせる方法など、様々なカスタマイズ方法が存在します。

これにより、コードの効率や可読性を向上させることが期待できます。

プロジェクトの要件に合わせてcompletionHandlerを適切に活用し、高品質なアプリケーションの開発を目指しましょう。