ログを表示するNSTextViewを作成する


概要

ログを表示するアプリを作成しました。
右側にあるOperationボタンの操作を行うと、ボタンの文字に合わせたログが表示されます。
(実際はプログラム処理の結果をログで表示するために使用します。)

押下したOperationボタンに応じてUI上にログを表示します。

[DEBUG][TRACE]に関してはログファイルにのみ表示します。(ユーザに知らせる必要のない情報のため)

また同時にappと同じ場所にログファイルを書き出しています。

GitHub

コードの詳細はGitHubを参照ください。

実装-UI部分

ペタペタと部品を貼り付けます。

今回ログの表示には以下の「Text View」(NSScrollView)を使用します。

ログの見た目を揃えるため、フォントは等幅フォントを使用します。
今回はFamily名をOsakaに設定、Styleを等幅とします。

また右側のOperationの文字の下に並んでいるのは、NSButtonのMatrixです。
以下の通りIBActionを使って押下されたボタンを取得します。

NSButtonのMatrix押下時の動作
// 画面右のOperationボタンが押下された場合
- (IBAction)operationButtonPush:(NSMatrix *)sender {
    int selectedRow = (int)sender.selectedRow;
    ...
}

実装-プログラム部分

コードの先頭で必要な変数をローカル変数として定義しています。

ログレベルに関しては、以下の記事を参考にしました。
ログ設計指針

AppDelegate.m
static NSString *const kLogFileName = @"sample.log"; // 出力ログ名

// ログ表示の調節用
static NSString *const kWhiteSpaceAdjustment = @"                              ";
static NSString *const kSeparateLine = @"---------------------------------------";

// ログのレベル
// コンソール・ファイルの両方に出力
static NSString *const kLogLevel_Fatal = @"[FATAL]"; // プログラムの異常終了を伴うようなもの。
static NSString *const kLogLevel_Error = @"[ERROR]"; // 予期しないその他の実行時エラー
static NSString *const kLogLevel_Warn  = @"[WARN] "; // 廃要素となったAPIの使用、APIの不適切な使用、エラーに近い事象など。異常とは言い切れないが正常ではない予期しない問題
static NSString *const kLogLevel_Info  = @"[INFO] "; // 実行時の何らかの注目すべき事象(開始や終了など)。メッセージ内容は簡潔に止めるべき
// コンソールのみに出力
static NSString *const kLogLevel_Debug = @"[DEBUG]"; // システムの動作状況に関する詳細な情報
static NSString *const kLogLevel_Trace = @"[TRACE]"; // デバッグ情報よりも、更に詳細な情報

またバインドするオブジェクトはNSScrollViewではなくて、NSTextViewであることに注意します。

@property (unsafe_unretained) IBOutlet NSTextView *logTextView;

ログのTextViewへの書き込み部分

TextViewへの末尾への文字列追加に関しては、以下を参考にしました。
NSTextViewの末尾に文字を追加する方法

日付部分の詳細は下記が詳しい。
iPhone SDK 本体のローカライズの書式設定に合わせて日付時刻を取得する方法

/**
 @brief コメントをログビューに追加する(その際にログファイルの更新も併せて行う)
 @param message ログに表示する内容 level ログのレベル
 */
- (void)appendLogMessage:(NSString *)message logLevel:(NSString *)level {
    // ログ記録時刻
    NSDate          *logDate          = [NSDate date];
    NSDateFormatter *logDateFormatter = [[NSDateFormatter alloc] init];
    logDateFormatter.dateStyle        = NSDateFormatterMediumStyle;
    logDateFormatter.timeStyle        = NSDateFormatterMediumStyle;
    NSString        *logDateStr       = [logDateFormatter stringFromDate:logDate];
    NSMutableString *logMessage       = [NSMutableString stringWithFormat:@"%@ %@ %@\r\n", level, logDateStr, message];

    // アプリ画面のログ表示(ログレベルがDEBUG, TRACEならばUI上のログには表示しない)
    if ([level isEqualToString:kLogLevel_Fatal] ||
        [level isEqualToString:kLogLevel_Error] ||
        [level isEqualToString:kLogLevel_Warn]  ||
        [level isEqualToString:kLogLevel_Info] ) {
        [_logTextView setEditable:YES];
        [_logTextView setSelectedRange: NSMakeRange(-1, 0)]; // 文末を選択
        [_logTextView insertText:logMessage replacementRange:NSMakeRange(-1, 0)];    // 末尾にログを追加
        [_logTextView setEditable:NO];
    }

    // ログファイルの更新
    if (![self updateLogFileWithMessage:logMessage]) {
        NSString *message = @"ログの出力時にエラーが発生しました。";
        NSMutableString *logMessage = [NSMutableString stringWithFormat:@"%@ %@ %@\r\n", kLogLevel_Error, logDateStr, message];
        [_logTextView setEditable:YES];
        [_logTextView setSelectedRange: NSMakeRange(-1, 0)]; // 文末を選択
        [_logTextView insertText:logMessage replacementRange:NSMakeRange(-1, 0)];    // 末尾にログを追加
        [_logTextView setEditable:NO];
    }
}

ログファイルの更新

既存のログファイルの中身を読み取り、それにテキストを追加して上書き保存を行います。

/**
 @brief ログファイルの更新を行う
 @param addedMessage ログに追加する文字列
 */
- (BOOL)updateLogFileWithMessage:(NSString *)addedMessage {
    // appと同じ場所にログファイルを書き出す
    NSURL   *bundleURL  = [NSURL fileURLWithPath:[NSBundle.mainBundle.bundlePath stringByDeletingLastPathComponent]];
    NSURL   *logFileURL = [bundleURL URLByAppendingPathComponent:kLogFileName];
    NSError *error      = nil;

    NSString *newLogMessage = [NSString string];   // 更新後のログメッセージ

    // 既存のログファイル読み込み
    NSString *oldLogMessage = [[NSString alloc] initWithContentsOfURL:logFileURL
                                                             encoding:NSUTF8StringEncoding
                                                                error:&error];
    if (oldLogMessage.length == 0) {
        newLogMessage = [NSString stringWithString:addedMessage];
    } else {
        newLogMessage = [oldLogMessage stringByAppendingString:addedMessage];
    }
    // 外部ログファイルに出力
    if (![newLogMessage writeToURL:logFileURL
                         atomically:YES
                           encoding:NSUTF8StringEncoding
                              error:&error]) {
        NSLog(@"%@", error.localizedDescription);
        return NO;
    }
    return YES;
}
出力されるログ
[INFO]  Mar 20, 2019 20:14:14 ---------------------------------------
[INFO]  Mar 20, 2019 20:14:14 *** アプリケーションが起動しました ***
[FATAL] Mar 20, 2019 20:14:18 Operationが実行されました。
[ERROR] Mar 20, 2019 20:14:19 Operationが実行されました。
[WARN]  Mar 20, 2019 20:14:20 Operationが実行されました。
[INFO]  Mar 20, 2019 20:14:21 Operationが実行されました。
[DEBUG] Mar 20, 2019 20:14:22 Operationが実行されました。
[TRACE] Mar 20, 2019 20:14:23 Operationが実行されました。
[ERROR] Mar 20, 2019 20:14:23 Operationを実行します。
                              長いログが出力されています…
                              長いログが出力されています…
                              長いログが出力されています…
                              Operationが終了しました。
[INFO]  Mar 20, 2019 20:14:25 *** アプリケーションを終了します… ***