Привет! Меня зовут Владислав Даниелян, я iOS-разработчик в AGIMA. Предлагаю немного поговорить о принтах. Это одна из первых и наиболее используемых функций, с которой начинаются первые шаги в разработке у любого новичка:
print("Hello world")
Цель статьи — сэкономить время начинающих разработчиков, уберечь их от бесконечного потока непонятных сообщений в консоли и от нервов, потраченных на поиск «той самой» строки, которая всё объясняет. Мы разберем виды принтов и напишем свой небольшой логгер, который можно внедрить сразу, параллельно чтению.

Логирование — важный инструмент в арсенале разработчика. Оно помогает систематизировать наши сообщения (коих со временем может появиться огромное количество), дает возможности фильтрации и многое другое. В этой статье посмотрим, какие инструменты от Apple у нас в распоряжении. А начнем с Print.
Print — самая базовая функция, выводящая текст в консоль Xcode. Она часто используется для дебаггинга и неплохо справляется с задачей, когда решать приходится простые проблемы. Но многие сталкивались с ситуацией, когда сообщения в Print становятся слишком громоздкими или когда их становится слишком много по всему приложению. В этих случаях консоль превращается в сплошную стену сложночитаемого текста.
Поэтому ниже рассмотрим альтернативы Print, но для нача��а разберемся с разновидностями самой Print, посмотрим, чем отличаются друг от друга Print, DebugPrint и Dump, поговорим об их достоинствах и недостатках, а также узнаем, что такое Logger и OSLog и напишем их базовую имплементацию для вашего проекта.
Начнем с простого. Так выглядит наша функция:
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. Стараемся логировать только по необходимости и не забываем им пользоваться только при дебаге.
Вот и всё. Мы рассмотрели виды принта с их достоинствами и недостатками и вспомнили виды логирования. А еще с минимумом усилий сделали процесс отладки работы приложения более легким и понятным.
Эту статью можно считать стартовой точкой. Для дальнейшего изучения оставлю документацию:
Когда ознакомитесь, попробуйте поэкспериментировать самостоятельно. Это обширная тема, и она достойна более глубокого изучения.
Если у вас остались вопросы — задавайте в комментариях. А если вас интересуют новости мобильной разработки — подписывайтесь на телеграм-канал моего коллеги Саши Ворожищева.
