<はじめに>

どうも!
推しなめろうは鯛(塩×梅×しそ)、さとこ(♂)です。
(美味なので騙されたと思って作ってみてね。
 
前回は「NSLog」、「os_Log」について漁りましたね。
今回は待ちに待ったログ出力機能のカスタマイズです。
この記事もいよいよ佳境を迎えます!
(大袈裟。
 
<過去の記事>
Swift 3系、Xcode8におけるログ出力関連機能を漁ってみた~その1~
Swift 3系、Xcode8におけるログ出力関連機能を漁ってみた~その2~
Swift 3系、Xcode8におけるログ出力関連機能を漁ってみた~その3~
 

<本記事の内容>

【今回の内容】
・さとこの3分カスタマイズ
・Xcodeのデバッガでログ出力

標準ログ出力関数の基本、応用と、「NSLog」、「os_log」等、そこそこハイスペックなログ出力関数の漁りを経て何がどんな感じで出力されるかはそれなりに見えてきましたが、ログで欲しい情報としては物足りない感があります。
(欲張るさとこ。
 
そこでコレ!
 

<便利なリテラル式>

ログ出力時に以下のリテラル式を使用することで出力情報をホクホクにすることができそうです。

#file : ソースファイルのファイルパス
#line : 行番号
#column : 列番号(行の中で左から何文字目か ※空文字含む)
#function : 関数名
参考:The Swift Programming Languageより

ふむふむ。
独自のログ出力関数を定義する際に重宝しそうですね。
 

<さとこの3分カスタマイズ>

はい、待ちに待ったさとこの3分カスタマイズのコーナーです。
テテテッテンテッテンテン!
うん。3分じゃ絶対に無理。
 

◆ サンプル(リテラル式全部使っちゃえ版)

// サンプル(リテラル式全部使っちゃえ版)
class Logger {
    static func Debug(msg: Object,
                      file: String = #file,
                      func: String = #function,
                      line: Int = #line,
                      column: Int  = #Column) {
        print("[\(file)] [\(func)] [\(line)] [\(column)] : \(msg)")
    }
}

// 呼び出し元での使い方
// class ViewController/override func viewDidLoad()
Logger.Debug(msg: "ログテスト")

// 出力結果(xx = 呼び出し側の行番号、yy = 列番号)
[OSLog_Sample/OSLog_Sample/LogSample/ViewController] [viewDidLoad()] [xx] [yy] : ログテスト

出力値の直前にファイル名などの詳細情報をログ出力するサンプルになります。
個人的に「行番号」さえあれば事足りるので、「列番号」は使いません。
(列番号の用途がイマイチピンとこない。
 
また、ファイル名がやたら長いので「タイムスタンプ」+「#file」+「#function」+「#line」+「出力したい値」とかやってたら高確率で折り返し発生します。
(13インチでは表示しきれないこと多々あり。
 
てことで、

<対策>

  • Xcodeの「preferences」→「Font&Size」からコンソールのフォントサイズを変更する
  • 画面解像度上げるアプリ使う

(どちらも顔が画面に近づき過ぎない程度にしときましょう。
 
とりあえずはタイムスタンプ、出力元のクラス名と関数名、行数があれば満足でしょうか。
ファイルパスがどうしても出力したい!でも短めで!
という要望があるのであればファイルパスを最低限出力したいとこで切り取っちゃえば良いんですけどね。
 
そして既にお気づきかと思いますが、タイムスタンプとクラス名のリテラル式は提供していない様子。
(あっても良いのになあ…
 
ということで、無いなら作っちゃえ!
 
まずは出力したい情報を精査してみます!

【出力したい情報】
・タイムスタンプ
・クラス名
・関数名
・行数
・出力したいデータ値

出揃いましたかね。
では手始めにタイムスタンプから!

/// タイムスタンプ取得(String -> Date)
/// - Parameter strDate: 文字列型日付
/// - Returns: タイムスタンプ(Date型)
static func stringToDate(strDate: String) -> Date {
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd HH:mm:ss"
    return df.date(from: strDate)!
}

/// タイムスタンプ取得(Date -> String)
/// - Parameter date: Date型日付
/// - Returns: タイムスタンプ(String型)
static func dateToString(date: Date) -> String {
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd HH:mm:ss"
    return df.string(from: date)
}

フォーマット部分(dateFormat)は好きなように変えましょ。
stringToDate()はオマケ。
 
次にクラス名を考えてみます!

// クラス名取得方法その1
let view = UIView()
NSStringFromClass(type(of: view))

// クラス名取得方法その2
String(describing: type(of: self))

上記のように記述して出力元で引数に指定するのもありですが、毎回記述するのはスマートじゃないですね。
ということで、「NSObject」を拡張して作成してみます。

// クラス名取得
extension NSObject {
    static var className: String {
        return NSStringFromClass(self).components(separatedBy: ".").last!
    }

    var className: String {
        return NSStringFromClass(type(of: self)).components(separatedBy: ".").last!
    }
}

以上を踏まえた上で再び作成してみます。
 

◆ サンプル(スマートにいこうぜ版)

// ログ出力関数サンプル(スマートにいこうぜ版)
class Logger {
    public static func Debug(className: String,
                             msg: Any,
                             func: String = #function,
                             line: Int = #line) {
        print("[\(dateToString(Date())] [\(className)] [\(func)] [\(line)] : \(msg)")
    }
}

// 呼び出し元での使い方
// class ViewController/override func viewDidLoad()
Logger.Debug(className: className, "ログテスト")

// 出力結果(xx = 呼び出し側の行数)
[2017-08-17 23:23:23] [ViewController] [viewDidLoad()] [xx] : ログテスト

出力結果の見栄えがよくなり、欲しい情報もわかりやすく出力できました!
欲を言えば、「os_log」で使用している「INFO」や「ERROR」のようなログレベルも欲しいな…。
 
せっかくなので追加してみます!
 

◆ サンプル(スマートにいこうぜ版:改)

/// (ログユティリティクラス)
public class Logger {

    /// ログタイプ列挙型
    /// - verbose: "VERBOSE"
    /// - info: "INFO"
    /// - debug: "DEBUG"
    /// - error: "ERROR"
    /// - warning: "WARNING"
    /// - dump: "DUMP"
    /// - `default`: "DEFAULT"
    enum Types: String {
        case verbose = "VERBOSE"
        case info = "INFO"
        case debug = "DEBUG"
        case error = "ERROR"
        case warning = "WARNING"
        case dump = "DUMP"
        case `default` = "DEFAULT"
    }

    /// デバッグログ出力
    /// - Parameters:
    ///   - level: ログ出力タイプ
    ///   - className: クラス名
    ///   - msg: 出力対象データ
    ///   - isAdditionalInfo: 埋め込み形式での出力
    ///                      (true = 追加情報出力/false = 値のみ出力)
    ///   - function: 関数
    ///   - line: 行数
    public func log<T:Any>(level: Level,
                         class className: String,
                         msg: T,
                         isAdditionalInfo: Bool = true,
                         function: String = #function,
                         line: Int = #line) {
        if level == .debug {
            debugPrint(msg)
        } else if level == .dump {
            dump(msg)
        } else if level == .default {
            print(msg)
        } else {
            if isAdditionalInfo {
                switch level {
                case .verbose, .info, .error, .warning:
                    print("[\(dateToString(date: Date()))] [\(className)] [\(function)] [\(line)] [\(level.rawValue)]: \(msg)")
                default: break
                }
            } else {
                switch level {
                case .verbose, .info, .error, .warning:
                    print(msg)
                default: break
                }
            }
        }
    }

    /// タイムスタンプ取得(Date -> String)
    /// - Parameter date: Date型日付
    /// - Returns: タイムスタンプ(String型)
    static func dateToString(date: Date) -> String {
        let df = DateFormatter()
        df.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return df.string(from: date)
    }
}

 

◆ 呼び出し元での使い方

// 呼び出し元での使い方
//
//  ViewController.swift
//  SampleLogTest

import UIKit

class ViewController: UIViewController {

    let logLevel = "ログレベルは"
    let array: [Any] = [1, "one", true, self.view.frame]

    override func viewDidLoad() {
        super.viewDidLoad()

        logger.log(level: .verbose, class: className, msg: logLevel + Logger.Level.verbose.rawValue + "です。")
        logger.log(level: .info, className: className, msg: logLevel + Logger.Level.info.rawValue + "です。")
        logger.log(level: .error, className: className, msg: logLevel + Logger.Level.error.rawValue + "です。")
        logger.log(level: .warning, className: className, msg: logLevel + Logger.Level.warning.rawValue + "です。")
        logger.log(level: .default, className: className, msg: logLevel + Logger.Level.default.rawValue + "です。")
        logger.log(level: .debug, className: className, msg: logLevel + Logger.Level.debug.rawValue + "です。")
        logger.log(level: .dump, className: className, msg: logLevel + Logger.Level.default.rawValue + "です。")
        logger.log(level: .dump, className: className, msg: array)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

 

◆ 出力結果

// 出力結果
[2017-08-18 00:25:30] [ViewController] [viewDidLoad()] [21] [VERBOSE]: ログレベルはVERBOSEです。
[2017-08-18 00:25:30] [ViewController] [viewDidLoad()] [22] [INFO]: ログレベルはINFOです。
[2017-08-18 00:25:30] [ViewController] [viewDidLoad()] [23] [ERROR]: ログレベルはERRORです。
[2017-08-18 00:25:30] [ViewController] [viewDidLoad()] [24] [WARNING]: ログレベルはWARNINGです。
ログレベルはDEFAULTです。
"ログレベルはDEBUGです。"
- "ログレベルはDEFAULTです。"
▿ 4 elements
  - 1
  - "one"
  - true
  ▿ (0.0, 0.0, 414.0, 736.0)
    ▿ origin: (0.0, 0.0)
      - x: 0.0
      - y: 0.0
    ▿ size: (414.0, 736.0)
      - width: 414.0
      - height: 736.0

○ポイント

  • 第一引数に列挙型(enum)を利用することで、ログレベルの選定が可能
  • 第一引数に指定したログレベルに合わせ、「print」、「debugPrint」、「dump」の使い分けが可能
  • 第四引数にデフォルト値が設定されているisEmbeddedを指定することで、埋め込み形式で出力するか否かの選定が可能(デフォルトは埋め込み有り)

列挙型とswitchを利用することで、多少Swiftらしい書き方になった気がします。
可読性も悪くはなさそうです。
(主観
ログレベルとしてのニュアンスは異なりますが、「debugPrint」と「dump」を追加してます。
(漁った結果、愛着湧いて無理やり追加
ただ、この2つに関しては、文字列補完によりエスケープシーケンスが付与されることを避けるため、埋め込み無しで値のみ出力するようにしてます。
(見づらいからね。
  
また、当サンプルでは埋め込みの有無で追加情報の調整をしていますが、タイムスタンプだけにしたい!クラスだけ欲しい!といった要望があれば、フラグ追加することで細かく分解することも可能です。
 
では最後に、実際にさとこが使用しているロガーをご紹介します。
(誰得。
 

◆ サンプル(スマートにいこうぜ版:神)

(え

/// Loggerクラス
open class Logger {

    private var isLoggerOn: Bool = true

    private var isTimeOn:  Bool
    private var isClassOn: Bool
    private var isFuncOn:  Bool
    private var isLineOn:  Bool
    private var isLevelOn: Bool

    private var TIME:  String = ""
    private var CLASS: String = ""
    private var FUNC:  String = ""
    private var LINE:  String = ""
    private var LEVEL: String = ""

    /// ログ出力内容切り替え初期化処理(デフォルトはすべて:ON)
    /// - Parameters:
    ///   - isLoggerOn: 【ON/OFF】ログ出力
    ///   - isTimeOn:   【ON/OFF】タイムスタンプ出力
    ///   - isClassOn:  【ON/OFF】クラス名出力
    ///   - isFuncOn:   【ON/OFF】関数名出力
    ///   - isLineOn:   【ON/OFF】行数出力
    ///   - isLevelOn:  【ON/OFF】ログレベル出力
    init(isLoggerOn: Bool, isTimeOn: Bool = true, isClassOn: Bool = true, isFuncOn: Bool = true, isLineOn: Bool = true, isLevelOn: Bool = true) {
        self.isLoggerOn = isLoggerOn
        self.isTimeOn   = isTimeOn
        self.isClassOn  = isClassOn
        self.isFuncOn   = isFuncOn
        self.isLineOn   = isLineOn
        self.isLevelOn  = isLevelOn
    }

    /// ログタイプ列挙型
    /// - info:      "INFO"
    /// - debug:     "DEBUG"
    /// - error:     "ERROR"
    /// - warning:   "WARNING"
    /// - dump:      "DUMP"
    /// - `default`: "DEFAULT"
    public enum Level: String {
        case verbose   = "VERBOSE"
        case info      = "INFO"
        case debug     = "DEBUG"
        case error     = "ERROR"
        case warning   = "WARNING"
        case dump      = "DUMP"
        case `default` = "DEFAULT"
    }

    /// デバッグログ出力
    /// - Parameters:
    ///   - level: ログ出力タイプ
    ///   - className: クラス名
    ///   - msg: 出力対象データ
    ///   - isModeSI: 埋め込み形式での出力
    ///     1. SI:(String Interpolation)
    ///     2. (true:詳細出力/false:値のみ出力)
    ///   - function: 関数
    ///   - line: 行数
    public func log<T:Any>(level: Level,
                    class className: String,
                    msg: T,
                    isModeSI: Bool = true,
                    function: String = #function,
                    line: Int = #line) {

        TIME  = isTimeOn  ? "[\(dateToString(date: Date()))] " : ""
        CLASS = isClassOn ? "[\(className)] " : ""
        FUNC  = isFuncOn  ? "[\(function)] " : ""
        LINE  = isLineOn  ? "[\(line)] " : ""
        LEVEL = isLevelOn ? "[\(level.rawValue)]" : ""

        if self.isLoggerOn {
            if level == .debug {
                debugPrint(msg)
            } else if level == .dump {
                dump(msg)
            } else if level == .default {
                print(msg)
            } else {
                if isModeSI {
                    if (!isTimeOn && !isClassOn && !isFuncOn && !isLineOn && !isLevelOn) {
                        print("\(msg)")
                    } else {
                        print(TIME + CLASS + FUNC + LINE + LEVEL + ": \(msg)")
                    }
                } else {
                    switch level {
                    case .verbose, .info, .error, .warning:
                        print(msg)
                    default: break
                    }
                }
            }
        }
    }

    /// タイムスタンプ取得(Date -> String)
    /// - Parameter 
    ///     - date: Date型日付
    ///     - isMili: ミリ秒制御(デフォルト出力)
    /// - Returns: タイムスタンプ(String型)
    static func dateToString(date: Date, isMili: Bool = true) -> String {
        let df = DateFormatter()
        if isMili {
            df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
        } else {
            df.dateFormat = "yyyy-MM-dd HH:mm:ss"
            return df.string(from: date)
        }
    }
}

 

◆ 呼び出し元での使い方

// 呼び出し元での使い方
//
//  ViewController.swift
//  SampleLogTest

import UIKit

public let logger = Logger(
                           isLoggerOn: true,
                           isTimeOn: true,
                           isClassOn: true,
                           isFuncOn: true,
                           isLineOn: true,
                           isLevelOn: true)

class ViewController: UIViewController {

    let logLevel = "ログレベルは"
    let array: [Any] = [1, "one", true, self.view.frame]

    override func viewDidLoad() {
        super.viewDidLoad()

        logger.log(level: .verbose, class: className, msg: logLevel + Logger.Level.verbose.rawValue + "です。")
        logger.log(level: .info,    class: className, msg: logLevel + Logger.Level.info.rawValue    + "です。")
        logger.log(level: .error,   class: className, msg: logLevel + Logger.Level.error.rawValue   + "です。")
        logger.log(level: .warning, class: className, msg: logLevel + Logger.Level.warning.rawValue + "です。")
        logger.log(level: .default, class: className, msg: logLevel + Logger.Level.default.rawValue + "です。")
        logger.log(level: .debug,   class: className, msg: logLevel + Logger.Level.debug.rawValue   + "です。")
        logger.log(level: .dump,    class: className, msg: logLevel + Logger.Level.dump.rawValue    + "です。")
        logger.log(level: .dump,    class: className, msg: array)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

 

◆ 出力結果

// 出力結果
[2017-08-18 01:25:30.277] [ViewController] [viewDidLoad()] [24] [VERBOSE]: ログレベルはVERBOSEです。
[2017-08-18 01:25:30.278] [ViewController] [viewDidLoad()] [25] [INFO]: ログレベルはINFOです。
[2017-08-18 01:25:30.278] [ViewController] [viewDidLoad()] [26] [ERROR]: ログレベルはERRORです。
[2017-08-18 01:25:30.278] [ViewController] [viewDidLoad()] [27] [WARNING]: ログレベルはWARNINGです。
ログレベルはDEFAULTです。
"ログレベルはDEBUGです。"
- "ログレベルはDUMPです。"
▿ 4 elements
  - 1
  - "one"
  - true
  ▿ (0.0, 0.0, 414.0, 736.0)
    ▿ origin: (0.0, 0.0)
      - x: 0.0
      - y: 0.0
    ▿ size: (414.0, 736.0)
      - width: 414.0
      - height: 736.0

○ポイント

  • 追加情報の出力制御を担うイニシャライザ
  • タイムスタンプの調整(ミリ秒対応)

インスタン生成時に、各出力制御フラグの【ON/OFF】の切り替えが可能です。
isLoggerOnは「os_log」の「OSLog.disabled」を参考にしています。
ログ出力が不要な場合はこいつをfalseに設定すればOK。

上記使用例では、「ViewController」でインスタンスを生成していますが、「AppDelegate」でグローバルに宣言して使用するのも有りだと思います。
 
また、ミリ秒対応に関してですが、秒単位だと差異がほとんど見受けられないことに気づいたので追加しました。
(重たい処理が無いってのもありますが、ほぼ一秒かからないで出力されています。

最後にドヤ顔でサンプルロガーあげましたが、世の中にはきっと…、否、絶対もっと素敵なログ出力関数が存在しますので参考程度に。
(さとこは気に入ってます。
 
今回触れませんが、上手な列挙型(enum)の使用法があるでしょうし、次の次の次くらいの記事あたりのために修行しておきます。
特に「Logger.Level.ログレベル.rawValue」に関しては毎回記述するのはくどいので、この辺りSwiftらしく省略できたらいいなと思います。
 
続いて、
Xcodeのデバッガを使用したログ出力について見ていきましょう。
 

<Xcodeのデバッガでログ出力>

タイトルのとおりです。
ブレイクポイントの設定を少し変えてあげるだけで「print」や「NSLog」などのログ出力関数を使用せずともコンソールにログ出力ができるようです。
(全然知らなかった。
 
ではでは早速見ていこうと思います!
参考:「【swift】Xcodeデバッガ入門
(参考というか、内容をそのまま引用させて頂きます。
 
以下の手順に沿って設定していきます。
 

<手順>

1:ブレイクポイント(Breakpoint)の設定
→値の確認をしたい箇所でブレイクポイントを設定
6ac55c8c a634 4aac 9458 603cbf235642

2:ブレイクポイント(Breakpoint)の編集
→ブレイクポイント上で、右クリックし「Edit Breakpoint ...」を選択
7ec78b81 dc89 459b 81e7 9b0c03baa0de

3:条件(Condition)の設定
→「Condition」に出力時の条件を設定
e.g.)(i % 2) == 0
 

4:動作(Action)の設定
→「Log Message」を選択
→「Log message to console」を選択し、出力したい値を設定
e.g.)@i@
Be97dfdb 9818 4211 ae15 bcb4ea4f5957
 

>>ちなみに...

Optionsの「Automatically continue after evaluating actions」にチェックを入れるとブレイクポイントでプログラムが停止しなくなります。これは他のIDEにない、とても便利な機能です。

だそうです!

// 出力結果
0
1
6
15
28
sum = 45

こんな感じで出力されました。
確かにブレイクポイントで一時停止することなく出力されました。
(さとこが信じられない場合はご自身でご確認ください...。
 
「Log message to console」=「print」の機能といったところでしょうか。
値の確認はしたいけど、ログ出力関数でコード量を増やしたくない。
といった場合は役立つかもしれませんね。
 

<まとめ>

  • 必要に応じてログ出力機能を選別し、カスタマイズする
  • 最初から便利な人気ライブラリ使う(今回はあえて触れてません)

<おわりに>

やはりアプリの開発内容や開発者の嗜好によって必要となる情報が異なるため、これが一番良い!というのは無さそうです。
ただ今回のようにログ出力に対する多様性を知ることで、多様な開発環境や仕様にも対応し易くなると思います。
 
以上!

Shere
  • はてなブログ
  • Twitter
  • Facebook
Swift 3系、Xcode 8におけるログ出力関連機能を漁ってみた〜その4〜

Writer

  • Name

    さとこ

  • Position

    どこにもあと2歩くらい足りないシャイなエンジニア

  • Profile

    C/C++/C#/Objective-C/Swift/Java/Install Shiled Script言語/Oracle DB等。 触りましましたよ、ちょんちょんって。 PHP、始めました。