Есть ли способ создать / извлечь массив представлений с помощью @ViewBuilder в SwiftUI
Я пытаюсь создать простой struct
который принимает массив представлений и возвращает обычный VStack
содержащие эти просмотры, за исключением того, что все они расположены по диагонали.
Код:
struct LeaningTower<Content: View>: View {
var views: [Content]
var body: some View {
VStack {
ForEach(0..<views.count) { index in
self.views[index]
.offset(x: CGFloat(index * 30))
}
}
}
}
Теперь это отлично работает, но меня всегда раздражает, когда я вызываю это:
LeaningTower(views: [Text("Something 1"), Text("Something 2"), Text("Something 3")])
Перечисление представлений в подобном массиве кажется мне чрезвычайно странным, поэтому мне было интересно, есть ли способ использовать @ViewBuilder
позвонить мне LeaningTower
структура как это:
LeaningTower { // Same way how you would create a regular VStack
Text("Something 1")
Text("Something 2")
Text("Something 3")
// And then have all of these Text's in my 'views' array
}
Если есть способ использовать @ViewBuilder
дайте мне знать, чтобы создать / извлечь массив представлений.
(Даже если это невозможно использовать @ViewBuilder
буквально все, что сделает его аккуратнее, очень поможет)
4 ответа
У меня работает общая версия, поэтому нет необходимости создавать несколько инициализаторов для кортежей разной длины. Кроме того, просмотры могут быть любыми (вы не ограничены для каждого
View
быть однотипными).
Вы можете найти Swift Package, который я сделал для этого, в GeorgeElsham / ViewExtractor . Код немного отличается от этого ответа, поэтому прочтите
README.md
сначала для примера.
Вернемся к ответу, пример использования:
struct ContentView: View {
var body: some View {
LeaningTower {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle")
}
}
}
Определение вашего взгляда:
struct LeaningTower: View {
private let views: [AnyView]
init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
views = content().getViews
}
var body: some View {
VStack {
ForEach(views.indices) { index in
views[index]
.offset(x: CGFloat(index * 30))
}
}
}
}
TupleView
extension (AKA, где происходит вся магия):
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Результат:
буквально все, что сделает его аккуратнее, очень поможет
Что ж, это приблизит вас:
Добавь это init
на ваш LeaningTower
:
init(_ views: Content...) {
self.views = views
}
Тогда используйте это так:
LeaningTower(
Text("Something 1"),
Text("Something 2"),
Text("Something 3")
)
Вы можете добавить свое смещение как необязательный параметр:
struct LeaningTower<Content: View>: View {
var views: [Content]
var offset: CGFloat
init(offset: CGFloat = 30, _ views: Content...) {
self.views = views
self.offset = offset
}
var body: some View {
VStack {
ForEach(0..<views.count) { index in
self.views[index]
.offset(x: CGFloat(index) * self.offset)
}
}
}
}
Пример использования:
LeaningTower(offset: 20,
Text("Something 1"),
Text("Something 2"),
Text("Something 3")
)
Я наткнулся на это, пытаясь придумать нечто подобное. Так что ради блага потомков и других бьются головами о подобных проблемах.
Лучший способ получить дочерние смещения, который я нашел, - это использовать следующий механизм набора текста Swift. например
struct LeaningTowerAnyView: View {
let inputViews: [AnyView]
init<V0: View, V1: View>(
@ViewBuilder content: @escaping () -> TupleView<(V0, V1)>
) {
let cv = content().value
inputViews = [AnyView(cv.0), AnyView(cv.1)]
}
init<V0: View, V1: View, V2: View>(
@ViewBuilder content: @escaping () -> TupleView<(V0, V1, V2)>) {
let cv = content().value
inputViews = [AnyView(cv.0), AnyView(cv.1), AnyView(cv.2)]
}
init<V0: View, V1: View, V2: View, V3: View>(
@ViewBuilder content: @escaping () -> TupleView<(V0, V1, V2, V3)>) {
let cv = content().value
inputViews = [AnyView(cv.0), AnyView(cv.1), AnyView(cv.2), AnyView(cv.3)]
}
var body: some View {
VStack {
ForEach(0 ..< inputViews.count) { index in
self.inputViews[index]
.offset(x: CGFloat(index * 30))
}
}
}
}
Использование будет
struct ContentView: View {
var body: some View {
LeaningTowerAnyView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
}
}
}
Он также будет работать с любым представлением, а не только с текстом, например
struct ContentView: View {
var body: some View {
LeaningTowerAnyView {
Capsule().frame(width: 50, height: 20)
Text("Something 2").border(Color.green)
Text("Something 3").blur(radius: 1.5)
}
}
}
Обратной стороной является то, что для каждого дополнительного представления требуется собственный инициализатор, но похоже, что это тот же подход, который Apple использует для своих стеков.
Я подозреваю, что что-то основанное на Mirror и убеждение Swift динамически вызывать переменное количество методов с разными именами может работать. Но это становится страшно, и примерно неделю назад у меня закончилось время;-) . Было бы очень интересно, если бы у кого-нибудь есть что-нибудь более элегантное.
Вы должны сделать заказ init
за LeaningTower
который принимает ViewBuilder
.
struct LeaningTower<Content>: View where Content: View {
var views: () -> Content
init(@ViewBuilder views: @escaping () -> Content) {
self.views = views
}
var body: some View {
VStack(content: views)
}
}
Применение:
struct ContentView: View {
var body: some View {
LeaningTower {
Text("Something 1")
Text("Something 2")
Text("Something 3")
}
}
}