Привет! Меня зовут Владислав Даниелян, я iOS-разработчик в AGIMA. Предлагаю немного поговорить о принтах. Это одна из первых и наиболее используемых функций, с которой начинаются первые шаги в разработке у любого новичка:

print("Hello world")

Цель статьи — сэкономить время начинающих разработчиков, уберечь их от бесконечного потока непонятных сообщений в консоли и от нервов, потраченных на поиск «той самой» строки, которая всё объясняет. Мы разберем виды принтов и напишем свой небольшой логгер, который можно внедрить сразу, параллельно чтению.

Логирование — важный инструмент в арсенале разработчика. Оно помогает систематизировать наши сообщения (коих со временем может появиться огромное количество), дает возможности фильтрации и многое другое. В этой статье посмотрим, какие инструменты от Apple у нас в распоряжении. А начнем с Print.

Print — самая базовая функция, выводящая текст в консоль Xcode. Она часто используется для дебаггинга и неплохо справляется с задачей, когда решать приходится простые проблемы. Но многие сталкивались с ситуацией, когда сообщения в Print становятся слишком громоздкими или когда их становится слишком много по всему приложению. В этих случаях консоль превращается в сплошную стену сложночитаемого текста.

Поэтому ниже рассмотрим альтернативы Print, но для начала разберемся с разновидностями самой Print, посмотрим, чем отличаются друг от друга Print, DebugPrint и Dump, поговорим об их достоинствах и недостатках, а также узнаем, что такое Logger и OSLog и напишем их базовую имплементацию для вашего проекта.

Print

Начнем с простого. Так выглядит наша функция:

func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

items — содержимое принта: то, что мы хотим напечатать.
separator — разделитель, вставляется между элементами.
terminator — будет вставлен за последним элементом из Items.

Print — это, пожалуй, самый легкий в использовании способ вывести сообщение. Правда, не очень подробное. Используется, как правило, для легкого дебага (с мелкими объектами). В релизном коде он оставаться не должен.

Задерживаться на нем долго не будем. Просто посмотрим на небольшой пример:

print(1, 2, separator: ", ", terminator: ".")
// 1, 2.

Так работает обычный принт. Зеленые цифры — текст, который мы получаем от функции.

DebugPrint

func debugPrint(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

Очень похож на обычный принт, но отличается тем, что предоставляет дополнительную информацию о печатаемых объектах:

print(1...5)
// 1...5

debugPrint(1...5)
// ClosedRange(1...5)

let world = "world"

print("hello", world)
// hello world

debugPrint("hello", world)
// "hello" "world"

DebugPrint целесообразно использовать в соответствии с его названием — для дебага. Он покажет больше полезной информации о том, с каким типом объектов мы имеем дело. Поэтому по большей части он может стать заменой обычной Print в работе. Но рекомендую перед релизом удалить Print и его альтернативы.

Dump

func dump<T>(
    _ value: T,
    name: String? = nil,
    indent: Int = 0,
    maxDepth: Int = .max,
    maxItems: Int = .max
) -> T

Еще одна функция для распечатки сообщений в консоль, но на этот раз со значительными отличиями от предыдущих. Давайте разбираться!

Для примера будем использовать небольшой объект Movie:

struct Movie {
    let name: String
    let rating: Double
    let actors: [String]
}

let movie = Movie(
name: "Звездные войны", 
rating: 5.0, 
actors: ["Лиам Нисон", "Натали Портман"]
)

value — объект, который мы хотим распечатать.

dump(movie)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▿ actors: 2 elements
    - "Лиам Нисон"
    - "Натали Портман"

name — заголовок, с которым объект будет распечатан.

dump(movie, name: "Объект")
// Output:
▿ Объект: StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▿ actors: 2 elements
    - "Лиам Нисон"
    - "Натали Портман"

indent — отступ: чем больше этот параметр, тем правее будет выведено сообщение.

dump(movie, indent: 10)
// Output:
          ▿ StudyProject.Movie
            - name: "Звездные войны"
            - rating: 5.0
            ▿ actors: 2 elements
              - "Лиам Нисон"
              - "Натали Портман"

maxDepth — глубина, отражает, насколько подробно будет напечатана информация об объекте.

dump(movie, maxDepth: 1)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
  - rating: 5.0
  ▹ actors: 2 elements

maxItems — максимальное количество элементов с «полным» описанием.

dump(movie, maxItems: 2)
// Output:
▿ StudyProject.Movie
  - name: "Звездные войны"
    (2 more children)

При работе с объектами и массивами объектов Dump показывает себя лучше, чем Print и DebugPrint. Мы получаем гораздо более наглядный результат, можем повлиять на то, в каком виде будет представлена информация, избавиться от лишнего «шума» в консоли.

OSLog

Наш самый главный инструмент для ведения логов — OSLog. Логи бывают нескольких уровней с говорящим названием:

  • default — используется, если не указан другой конкретный уровень;

  • info — полезная, но не критично важная информация, не отобразится в Console.app (об этом далее);

  • debug — сообщения для отладки также не отображается в Console.app;

  • error — для логирования ошибок, в консоли Xcode выделяется желтым цветом;

  • fault — также для логирования ошибок, но уже критичных, в Xcode выделяется красным.

Напишем простой логгер, которым прямо сейчас можно будет воспользоваться в своем проекте. В дальнейшем можно использовать данную информацию как фундамент и расширять его возможности (сохранять логи в файл, замерять скорость выполнения функции и многое другое).

// Используем публичную функцию для ведения логов, в зависимости от ваших потребностей, реализовать можно разными способами
public func log(_ items: Any...,
                type: OSLogType = .default,                
                file: String = #file,
                function: String = #function) {
    formLog(items, type: type, file: file, function: function)
}

Для этой функции мы передаем тип, название файла и название функции. Кастомизировать это можно как угодно. Например, передавать номер строки или различные названия для subsystem, чтобы легче фильтровать логи по разным модулям. Обращу внимание, что в целях экономии ресурсов логи мы запускаем в дебаг-среде пользуясь #if DEBUG. Посмотрим, как функция реализована внутри:

private func formLog(_ items: [Any],
                     type: OSLogType,
                     file: String,
                     function: String) {
    #if DEBUG
    // Форматируем название файла(можно пропустить или сделать иначе)
    let lastSlashIndex = (file.lastIndex(of: "/") ?? String.Index(utf16Offset: 0, in: file))
    let nextIndex = file.index(after: lastSlashIndex)
    let filename = file.suffix(from: nextIndex).replacingOccurrences(of: ".swift", with: "")
    
    // subsystem, можно передать извне, чтобы отличать логи по модулям
    let subsystemName = "TestModule"
    
    // создаем лог, в качестве категории используем обработанное название файла
    let log = OSLog(subsystem: subsystemName, category: filename)
    
    // формируем сообщение
    let items = items.map {"\($0)"}.joined(separator: ", ")
    let formattedMessage = [function, items].joined(separator: " | ")
    
    os_log("%{public}s", log: log, type: type, formattedMessage)
    #endif
}

Итак, мы добавили логи в приложение, придали им читаемый вид. Что дальше? Конечно, нужно их прочитать. Логи мы можем видеть в консоли Xcode с разным выделением по цвету в зависимости от типа, но наибольшую пользу из них мы сможем извлечь с помощью нативного приложения для MacOS — Console.

Плюсы приложения по сравнению с обычной консолью:

  • приложение пока открыто,

  • сохраняет логи (в том числе и после перезапуска приложения),

  • в нем можно выставить удобные фильтры-шаблоны, чтобы быстро находить необходимые записи.

Запустим приложение на симуляторе. Откроем приложение и увидим следующую картину:

Выбираем наш девайс и нажимаем «Начать потоковую передачу».

Сразу же видим тонну не слишком информативных логов. Чтобы найти необходимые нам, воспользуемся фильтрацией в верхнем правом углу. Вписываем название нашего приложения, жмем Enter, выбираем «Библиотека». Теперь мы увидим собственные логи.

Поиск можно кастомизировать: добавить поиск по Subsystem, чтобы посмотреть на сообщения из определенного модуля, отфильтровать результаты по типу и т. д. Также поиск можно сохранить, а отображаемые поля настроить, например уберем «процесс» и добавим «подсистему».

Сохраняем поиск, настраиваем отображение полей и видим такой результат:

Все наши логи теперь на экране. Можем их просматривать, фильтровать, делиться в текстовом виде. Еще один плюс логирования перед принтами: логи не очищаются между перезагрузками вашего симулятора, поэтому можете сравнивать результаты и отслеживать изменения последовательно, в реальном времени

Logger

Более свежей альтернативой OSLog является Logger, доступный с iOS 14. 

Logger можно использовать как альтернативу OSLog. С помощью расширений (Extension) можно создать несколько логгеров, отвечающих за логирование разного функционала.

import OSLog

extension Logger {
    private static var subsystem = “com.agima.example”

    /// Логи network слоя
    static let network = Logger(subsystem: subsystem, category: "network")

    /// Логи вашего сервиса
    static let yourService = Logger(subsystem: subsystem, category: "your_service")
}

Logger от OSLog отличается в деталях. Это и разные уровни логирования, и возможности настройки логов. Разберем по порядку.

Уровни логирования:

  • notice;

  • info;

  • debug;

  • trace;

  • warning;

  • error;

  • fault;

  • critical.

Как мы видим, уровней здесь больше чем в OSLog, и некоторые из них отличаются по смыслу. В консоли Xcode, нажав на иконку правее «глаза» (на скрине ниже), можно отобразить метаданные сообщений. Таким образом наши логи будут выглядеть так:

В приложении Console они читаются идентично примеру из OSLog, поэтому перестраиваться не придется.

Еще одна отличительная особенность — возможность настроить приватность сообщений. Например:

Logger.network.info("Пользователь \(username, privacy: .private) добавил товар")

Из-за параметра Privacy Username будет скрыт, однако при использовании симулятора вы этого не увидите.

Что в итоге использовать

В заключение отмечу, что не стоит злоупотреблять логами в принципе. Чтобы избежать негативного влияния на производительность, лучше включать их в Debug-среде или по тогглу для конкретного пользователя, если возникает такая необходимость. Также стоит избирательно подходить к их добавлению и не логировать без надобности, чтобы избежать лишнего «шума» и облегчить процесс дебаггинга.

Но если необходимость всё-таки есть, то к выбору инструментов нужно подходить с учетом стоящих перед вами задач:

  • Print и его разновидности — для дебага в сочетании с Breakpoints. Не оставляем его в релизном коде. Вид принта (Dump, Debug и т. д.) выбираем в зависимости от того, что именно дебажим.

  • OSLog/Logger. Фреймворк для ведения логов выбираем исходя из минимальной версии iOS на проекте. Logger доступен начиная с iOS 14. Стараемся логировать только по необходимости и не забываем им пользоваться только при дебаге.

Вот и всё. Мы рассмотрели виды принта с их достоинствами и недостатками и вспомнили виды логирования. А еще с минимумом усилий сделали процесс отладки работы приложения более легким и понятным.

Эту статью можно считать стартовой точкой. Для дальнейшего изучения оставлю документацию:

Когда ознакомитесь, попробуйте поэкспериментировать самостоятельно. Это обширная тема, и она достойна более глубокого изучения.

Если у вас остались вопросы — задавайте в комментариях. А если вас интересуют новости мобильной разработки — подписывайтесь на телеграм-канал моего коллеги Саши Ворожищева.

Что еще почитать

Комментарии (4)


  1. FreeNickname
    13.06.2024 16:19
    +1

    Спасибо, совсем забыл про dump :)

    Добавлю, что Apple не рекомендует делать свои функции-обёртки вокруг OSLog / Logger, т.к., во-первых, они оптимизированы для того, чтобы лениво формировать сообщения в фоне (Вы в методе formLog создаёте formattedMessage сами), а во-вторых, позволяют прямо из сообщения в логе перейти в то место в коде, где оно было залогировано. Если вокруг системных функций сторонняя обёртка, кидать будет всегда в эту обёртку.


    1. Vdanielyan Автор
      13.06.2024 16:19
      +1

      Спасибо, хорошее замечание по поводу обертки! С учетом того, что мы используем логи только в дебаг-режиме, это не влияет на производительность критичным образом, однако, вызов функции стоит действительно вынести и воспользоваться советом Apple :)


      1. FreeNickname
        13.06.2024 16:19
        +1

        Там, кстати, на самом деле вообще всё интересно :)

        Для реализации нативного Swift-логирования хаки зашиты прямо в компиляторе. Я уже не помню подробностей того, что я хотел сделать, но, вроде бы, я хотел иметь либо свою обёртку вокруг системного логгера, которая прокидывает ему все параметры вроде точки вызова и т.д. (ведь наверняка это реализовано через неявные параметры, думал я), либо сделать протокол, повторяющий интерфейс системного логгера, "подписать" системный логгер под этот протокол через extension, и таким образом иметь возможно работать везде с протоколом с простой возможностью подменить реализацию во время выполнения. Но оказалось, насколько я помню, что невозможно перебросить параметры в системный логгер, даже если у нас функция объявлена точно так же, потому что для системного логгера выполняется какая-то особая компиляторная магия.

        В итоге я удовольствовался typealias. У меня есть свой логгер, который повторяет интерфейс системного, и я повсюду пользуюсь alias-ом. Когда нужно, alias указывает на системный логгер, когда нужно – на кастомный. Но в динамике, понятное дело, уже не переключишь.


        1. Vdanielyan Автор
          13.06.2024 16:19
          +1

          typealias звучит как хороший выход из ситуации)