
Меня зовут Илья, я мобильный разработчик в Naumen. Моя основная специализация — iOS‑разработка. Я занимаюсь развитием мобильного клиента платформы Naumen Service Management Platform, а также Chat SDK в рамках Naumen Contact Center.

Илья
iOS-разработчик в Naumen
В работе мобильной команды регулярно появляются задачи, в процессе решения которых команда так или иначе сталкивается с трудностями. У меня это произошло, когда я занимался задачей на сжатие изображений перед отправкой на сервер.
В статье расскажу, как из этой задачи вырос подход к автоматической генерации экранов настроек: без ручного добавления каждого нового поля в интерфейс, с опорой на интроспекцию типов и метаданные у самих свойств.
Как из обычной задачи выросла проблема с настройками
Когда в работе появилась задача на сжатие изображений перед отправкой на сервер, основной проблемой казалась сама реализация: как сжимать, как настроить параметры, как подобрать нужное поведение.
Но довольно быстро возникла другая проблема: как проверять изменения в реальном времени, не пересобирая приложение после каждой правки.
Для разработчика это не так критично — пересобрать проект через Xcode можно за несколько минут. Однако в работе участвуют и другие роли:
аналитики на приемке
тестировщики при тестировании
Каждое изменение параметров означало необходимость пересборки приложения. В итоге тестирование любой мелкой настройки превращалось в цепочку действий с участием разработчика.
Стало очевидно, что нужен отдельный экран с настройками, где параметры можно менять прямо во время работы приложения.
С какой еще проблемой мы столкнулись
Сначала мы решили сделать экран настроек с переключателями, полями ввода и другими UI‑элементами, однако столкнулись со следующей ситуацией.
Каждый раз, когда появляется новая настройка или меняется существующая, разработчику нужно:
добавить свойство в модель настроек
добавить UI‑компонент на экран
прописать обработку
связать все с системой хранения
Это неудобно по нескольким причинам.
Растет объем дублирующего кода
Тратится дополнительное время
Любое расширение экрана требует новых изменений в кодовой базе
Такую систему можно собрать через фабрики: описывать структуру данных отдельно, а компоненты строить на ее основе, но это решает лишь часть задачи. Однако даже в этом случае остается ручная работа — при появлении новой настройки нужно идти в отдельную часть кода, добавлять ее описание и отдельно выводить на экран.
Мне хотелось прийти к модели, где разработчик просто описывает новую настройку, а все остальное система делает сама.
К какому решению в итоге мы пришли
В поиске решения я вспомнил о механизме Reflection, в других языках этот механизм может называться Introspection. Он позволяет анализировать структуру объектов во время выполнения программы.
В Swift возможности рефлексии ограничены, поэтому корректнее говорить об интроспекции, а не о полной рефлексии, потому что язык не позволяет изменять код в реальном времени. Но для нашей задачи этого оказалось достаточно.
На основе интроспекции данных удалось выстроить подход к созданию экранов настроек, который заметно сокращает трудозатраты и убирает значительную часть ручной работы при добавлении новых параметров.
Что меняется в подходе
Если коротко, идея такая: разработчик больше не описывает каждый UI‑элемент экрана отдельно вручную. Вместо этого он задает свойства объекта настроек и добавляет к ним нужные метаданные. Дальше система сама анализирует эти данные и генерирует соответствующие компоненты интерфейса.

Из чего состоит такая система
1. Метаданные настроек
Каждая настройка описывается как свойство объекта. Дополнительно используются аннотации, которые содержат метаданные:
читаемое название свойств настроек
зависимости между настройками
дополнительные параметры отображения
2. Механизм анализа типов данных (интроспекция)
Центральный механизм системы — анализ типов данных, который с помощью интроспекции анализирует структуру объекта настроек во время выполнения программы.
Он проходит по всем свойствам объекта настроек и извлекает:
тип данных конкретной настройки
метаданные из аннотаций
Полученная информация используется для построения интерфейса.
3. Система соответствия типов данных и UI-компонентов
Дальше система определяет, какой UI‑компонент нужен для конкретного типа.
Например:
для Bool — переключатель
для текстового значения — поле ввода
для числового значения — тоже поле ввода, но уже с ограничением на числовой ввод
За счет этого интерфейс строится не вручную, а на основе правил сопоставления.
4. Система обработки событий взаимодействия и UI-компонентов
После генерации интерфейса пользователь должен иметь возможность менять значения, а система — корректно эти изменения обрабатывать.
Для этого нужен обработчик событий, который принимает пользовательские действия, передает изменения в систему хранения данных и уведомляет о том, что данные изменились.
5. Система хранения и восстановления настроек
Чтобы настройки могли сохраняться между запусками приложения, можно использовать систему хранения. Она должна уметь сохранять данные при их изменении и восстанавливать при перезапуске системы.
Как выглядит процесс сборки экрана
Для автоматического создания экрана настроек сначала нужно объявить объект настроек и описать в нем необходимые свойства, добавив аннотации для дополнительных метаданных. После этого модель объекта настроек передается на сборку.
Сама сборка делится на четыре этапа: анализ объекта настроек, генерация UI‑модели, создание интерактивного интерфейса и обработка изменений.

Этап 1. Анализ объекта настроек
Получение метаинформации об объекте настроек с использованием интроспекции
Обход всех свойств объекта
Извлечение типов данных и метаданных из аннотаций
Этап 2. Генерация UI-модели
Создание секций настроек на основе группировки по объектам настроек
Генерация UI-моделей компонентов для каждого типа данных
Настройка связей между компонентами — зависимости
Применение метаданных — названия
Этап 3. Создание интерактивного интерфейса
Создание UI-элементов на основе сгенерированной модели
Настройка обработчиков событий для каждого компонента
Связывание с системой хранения для сохранения изменений
Реализация зависимостей между настройками
Этап 4. Обработка изменений
Перехват пользовательского ввода через обработчик событий
Валидация данных в соответствии с типом
Сохранение в хранилище с сериализацией
Обновление зависимых настроек при необходимости
Как это реализовано в Swift
Метаданные через Property Wrappers
Для добавления метаданных мы используем механизм Property Wrappers. Он позволяет обернуть свойства в дополнительную логику и сохранить прозрачный доступ к значению. Property Wrappers идеально подходит для нашего случая, так как он может добавлять метаданные прямо в объявление свойства, а не где‑то в отдельной части кода.
@propertyWrapper struct SettingInfo<Value>: SettingsProperty { var anyWrappedValue: Any { return wrappedValue } var wrappedValue: Value let readableTitle: String? let linkedToKey: String? init(wrappedValue: Value, title: String? = nil, linkedToKey: String? = nil) { self.wrappedValue = wrappedValue self.readableTitle = title self.linkedToKey = linkedToKey } } // Пример описание настроек class AppSettings { @SettingInfo(title: "Включить уведомления", linkedTokey: "isLoggedIn") var notificationsEnabled: Bool = true @SettingInfo(title: "Имя пользователя") var userName: String = "Guest" }
Анализ типов через Mirror
Для интроспекции используется Mirror API. Это встроенный механизм рефлексии, который позволяет анализировать структуру объектов во время выполнения.
А еще с помощью механизма можно:
автоматически обнаружить все свойства объекта настроек
извлечь их типы и метаданные
построить на основе этой информации UI‑модель
private func parseSetting( from child: Mirror.Child, for settingsClass: Any, title: String? = nil, linkedToKey: String? = nil, metaClass: String ) → SettingsSection.Setting? { let fieldTitle = child.label ?? "" let propertyTitle = title ?? fieldTitle let accessibilityId = "\(metaClass).\(propertyTitle)" switch child.value { case let value as String: let model = SettingsSection.Setting.Model<String, UITextField>( title: fieldTitle, value: value, key: accessibilityId, binding: { [weak self] textField in guard let self else { return } textField.accessibilityIdentifier = accessibilityId textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) } ) return .textField(model) case let value as Bool: let model = SettingsSection.Setting.Model<Bool, UISwitch>( title: fieldTitle, value: value, key: accessibilityId, binding: { [weak self] `switch` in guard let self else { return } `switch`.accessibilityIdentifier = accessibilityId `switch`.addTarget(self, action: #selector(self.switchValueChanged(_:)), for: .valueChanged) } ) return .switchAction(model) case let value as SettingsProperty: return parseSetting( from: (value.readableTitle, value.anyWrappedValue), for: settingsClass, title: String(propertyTitle.dropFirst()), linkedToKey: value.linkedToKey, metaClass: metaClass ) } }
Генерация UI-компонентов
Для построения интерфейса можно использовать:
UIKit
SwiftUI
В нашем проекте используется UIKit. В целом такой подход можно реализовывать и через SwiftUI, потому что декларативная модель там тоже хорошо ложится на идею автоматической сборки интерфейса. Но в нашем случае выбор UIKit продиктован самим проектом.
Ключевая часть здесь — создание фабрики компонентов. Она принимает уже сгенерированную UI-модель и создает нужный UI-компонент для конкретного типа настройки.
func buildCell() -> UITableViewCell { let cell = UITableViewCell(style: .default, reuseIdentifier: nil) cell.textLabel?.text = title cell.textLabel?.numberOfLines = 0 cell.selectionStyle = .none switch self { case .switchAction(let model): let `switch` = UISwitch(frame: .zero) `switch`.isOn = model.value cell.accessoryView = `switch` model.binding(`switch`) case .textFieldWithString(let model): let frame = CGRect(x: 0, y: 0, width: 150, height: 40) let textField = UITextField(frame: frame) textField.text = model.value textField.keyboardType = .default textField.textAlignment = .left textField.borderStyle = .roundedRect textField.placeholder = "Введите значение" cell.accessoryView = textField model.binding(textField) } return cell }
Обработка событий
Обработка событий — это критически важная часть нашей системы. Пользователь должен иметь возможность изменять настройки, а эти изменения должны корректно обрабатываться и сохраняться.
В Swift мы используем стандартный подход с назначением действия на UI‑компоненты — это классический подход iOS‑разработки. Каждый UI‑компонент может иметь целевой объект и действие, которое выполняется при взаимодействии. В других языках программирования эта задача чаще всего решается с помощью callback'ов.
Каждый UI‑компонент получает свое действие на изменение значения. Дальше система должна правильно интерпретировать пользовательский ввод и привести его к нужному типу.
Это особенно важно для текстовых полей. В одном случае текстовое значение должно сохраниться как строка, в другом — быть преобразовано в число, если настройка ожидает именно числовое представление.
Поэтому здесь критична типобезопасность: система должна не просто получать ввод, а проверять, что его можно корректно сохранить.
func handleAction(for control: UIControl, valueType: ValueType) { switch valueType { case .text: control.addTarget(self, action: #selector(textFieldDidChange), for: .valueChanged) case .boolean: control.addTarget(self, action: #selector(switchValueChanged), for: .valueChanged) } } @objc private func textFieldDidChange(_ sender: UITextField) { guard let accessibilityId = sender.accessibilityIdentifier else { return } updateStoredValue(sender.text, forKey: accessibilityId) } @objc private func switchValueChanged(_ sender: UISwitch) { guard let accessibilityId = sender.accessibilityIdentifier else { return } updateStoredValue(sender.isOn, forKey: accessibilityId) { [weak self] in guard let self else { return } delegate?.switchValueUpdated(for: accessibilityId) } }
Хранилище и восстановление данных
Для хранения настроек в Swift мы используем:
UserDefaults — для простых типов данных
NSKeyedArchiver — для преобразования сложных объектов в данные
Важно, что система хранения должна быть устойчивой к ошибкам. Данные могут повредиться, не загрузиться или отсутствовать вовсе. В таком случае настройки все равно должны отображаться и корректно работать со значениями по умолчанию.
Кроме того, система должна учитывать, что структура настроек со временем может меняться. Значит, сериализация и десериализация должны работать так, чтобы не терять уже сохраненные данные при изменении модели.
Ключевая особенность системы хранения — это автоматическая сериализация и десериализация различных типов данных. Нужно корректно обрабатывать как примитивные типы, так и сложные объекты, обеспечивая при этом обратную совместимость при изменении структуры настроек.
func retrieveValue(forKey key: String) throws -> Any { let isEditedKey = "\(key)\(isEditedSuffix)" if userDefaults.bool(forKey: isEditedKey) { if let object = userDefaults.object(forKey: key) { return try archiver.unarchive(object) } else { throw StorageError.notFound } } else { throw StorageError.notEdited } } func setValue(_ value: Any?, forKey key: String) throws { let isEditedKey = "\(key)\(isEditedSuffix)" let valueToStore = try archiver.archive(value) userDefaults.set(valueToStore, forKey: key) userDefaults.set(true, forKey: isEditedKey) }
Пример добавления настройки
Изначальное описание объекта настроек:
class StartupSettings { static let shared = StartupSettings() @SettingInfo(title: "Название приложения") var appName: String = "ChatSDK" @SettingInfo(title: "Отображать TabBar") var showTabBar: Bool = true } extension StartupSettings: PropertyAccess { func set(value: Any?, forKey key: String) { switch (value, key) { case (let value as String, "appName"): appName = value case (let value as Bool, "showTabBar"): showTabBar = value } } }
Что пользователь увидит в приложении:

Важное ограничение Swift, которое пришлось обойти
Здесь есть принципиальный момент. В Swift рефлексия ограничена: она не позволяет изменять значение напрямую через механизм интроспекции.
Поэтому для этой части был реализован отдельный интерфейс PropertyAccess, который закрывает этот пробел и позволяет работать со значениями так, как это нужно системе.
Пример добавления новой настройки для отображения вкладки с чатом
Настройка связана с отображением таббара. Включение и отключение настройки отображения таббара влияет на настройку открытия вкладки с чатом в таббаре:
class StartupSettings { static let shared = StartupSettings() @SettingInfo(title: "Название приложения") var appName: String = "ChatSDK" @SettingInfo(title: "Отображать TabBar") var showTabBar: Bool = true @SettingInfo( title: "Сразу открывать вкладку с чатом при показе TabBar", linkedToKey: "showTabBar" ) var openChatPageOnOpen: Bool = true } extension StartupSettings: PropertyAccess { func set(value: Any?, forKey key: String) { switch (value, key) { case (let value as String, "appName"): appName = value case (let value as Bool, "showTabBar"): showTabBar = value case (let value as Bool, "openChatPageOnOpen"): openChatPageOnOpen = value } } }

Что дает такой подход на практике
Поддержка разных языков
Подход легко переносится на другие языки программирования, где поддерживается интроспекция данных.

Расширяемость системы
Чтобы добавить новый тип в обработку, достаточно:
Добавить новый тип данных в анализатор
Создать соответствующий UI‑компонент
Настроить обработку взаимодействия с данным компонентом
После все свойства этого типа автоматически появятся на экране настроек.
Сокращение трудозатрат
Трудозатраты на добавление новых настроек сокращаются примерно на 80–90% по сравнению с ручной реализацией экрана и самих настроек. Разработчику достаточно описать новое свойство с аннотацией, и оно автоматически появляется в интерфейсе.

Уменьшение объема дублирующего кода
Больше не нужно отдельно идти в другую часть кода, добавлять описание, потом вручную выводить компонент и отдельно настраивать его поведение.
Типобезопасность
Если системе встретится неподдерживаемый тип данных, это не приведет к падению приложения: такая настройка будет пропущена и не отобразится, пока для нее не добавят нужную обработку.
Консистентный интерфейс
Все настройки одного и того же типа отображаются одинаково, а значит, для пользователя экран ведет себя предсказуемо.
Подход с автоматической генерацией UI-настроек через интроспекцию типов позволяет сократить объем ручной работы и дает разработчику возможность уделять больше времени бизнес-логике, а не ручному созданию интерфейсов.
В нашем случае он вырос из обыденной задачи — необходимости быстро проверять изменения без постоянной пересборки приложения. Но в итоге этот подход можно использовать не только для одной конкретной задачи, а для построения экранов настроек в целом.
