Есть ли способ создать / извлечь массив представлений с помощью @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")
        }
    }
}
Другие вопросы по тегам