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


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

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

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

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

![Mar-20-2019 10-16-18](https://i.imgur.com/QB7SENt.gif)

# GitHub
[コードの詳細はGitHubを参照ください。](https://github.com/pommdau/Qiita_LogTextView)

# 実装-UI部分

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

![-w944](https://i.imgur.com/zMNOMeF.jpg)

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

![](https://i.imgur.com/KhmFazm.jpg)

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

![](https://i.imgur.com/EnexclX.jpg)

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

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

# 実装-プログラム部分

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

ログレベルに関しては、以下の記事を参考にしました。
[ログ設計指針](https://qiita.com/nanasess/items/350e59b29cceb2f122b3)
   

``` ObjectiveC: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`であることに注意します。

``` ObjectiveC
@property (unsafe_unretained) IBOutlet NSTextView *logTextView;
```

## ログのTextViewへの書き込み部分
TextViewへの末尾への文字列追加に関しては、以下を参考にしました。
[NSTextViewの末尾に文字を追加する方法](http://nao-bamboo.hatenablog.com/entry/2015/12/15/100938)
   
日付部分の詳細は下記が詳しい。
[iPhone SDK 本体のローカライズの書式設定に合わせて日付時刻を取得する方法](http://blog-e.uosoft.net/entry/20090717/1247757636)

``` ObjectiveC
/**
 @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];
    }
}
```

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

``` ObjectiveC
/**
 @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 *** アプリケーションを終了します… ***
```