429просмотров
82.7%от подписчиков
11 ноября 2025 г.
Score: 472
Custom View Builders Думаю, многим из вас приходилось писать SwiftUI контейнеры, которые должны отображать список определённых View. Например, список из нескольких кнопок: struct ButtonsList: View { let models: [ButtonModel] var body: some View { VStack { ForEach(models, id: \.title) { model in Button(action: model.action) { Text(model.title) } .buttonStyle(.borderedProminent) } } }
} struct ButtonModel { let title: String let action: @MainActor () -> Void
} Однако в таком случае использование подобных View становится неудобным. Во всех местах, где используется этот контейнер, придётся создавать массивы: #Preview { ButtonsList(models: [ .init(title: "First Button", action: {}), .init(title: "Second Button", action: {}), ])
} Это выбивается из общемго стиля SwiftUI, а также работает неэффективно из-за постоянных аллокаций массивов. Есть и другой нюанс — ForEach. Ему нужны данные, соответствующие протоколу Identifiable. В примере выше для этого использовалось поле title, что, в общем-то, не самое верное решение. При каждом пересчёте View, содержащей ButtonsList, этот массив будет создаваться заново. Это означает, что любые автоматически сгенерированные внутри моделей идентификаторы также будут пересоздаваться. Поэтому не остаётся более надежного решения, кроме как заставить пользователя явно передавать id и дополнительно валидировать его уникальность: #Preview { ButtonsList(models: [ .init(id: 1, title: "Button", action: {}), .init(id: 2, title: "Button", action: {}), ])
} Можно ли как-то решить эту проблему? Если посмотреть на стандартные компоненты SwiftUI с похожей семантикой, такие как TabView, ToolbarContent или Scene, то можно заметить, что все они вместо массивов используют специальные @resultBuilder:
- TabContentBuilder
- ToolbarContentBuilder
- SceneBuilder Итак, кажется, решение найдено. Нужно написать свой @resultBuilder и воспользоваться им: @MainActor
@resultBuilder
struct ButtonsListBuilder1 { static func buildBlock( _ content: ButtonModel... ) -> some View { let enumeratedContent = Array(content.enumerated()) return ForEach(enumeratedContent, id: \.offset) { _, model in Button { ... } } }
} struct ButtonsList<Buttons: View>: View { @ButtonsListBuilder let buttons: Buttons var body: some View { VStack { buttons } }
} #Preview { ButtonsList { ButtonModel(title: "First Button", action: {}) ButtonModel(title: "Second Button", action: {}) }
} Отлично, теперь код выглядит полностью в стиле SwiftUI. Проблема с идентификаторами решена через индексы. В случае с @resultBuilder это безопасно, потому что список статичен и не может измениться. Но решение всё ещё не идеально. Проблема с созданием массивов не только осталась, но и усугубилась, потому что теперь их два. Наличие ForEach в этом решении тоже смущает, так как у нас на самом деле нет динамических списков — все View известны ещё на этапе создания ButtonsList.