ios SwiftUI -自动在"ForEach“的每个元素之间添加分隔符

31moq8wy  于 2023-02-26  发布在  iOS
关注(0)|答案(4)|浏览(228)

我使用ForEach来显示数组的内容,然后通过检查元素索引手动显示每个元素之间的分隔符。

struct ContentView: View {
    let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]

    var body: some View {
        VStack {
            /// array of tuples containing each element's index and the element itself
            let enumerated = Array(zip(animals.indices, animals))
            ForEach(enumerated, id: \.1) { index, animal in
                Text(animal)

                /// add a divider if the element isn't the last
                if index != enumerated.count - 1 {
                    Divider()
                        .background(.blue)
                }
            }
        }
    }
}

结果:

这是可行的,但是我希望有一种方法可以自动地在所有地方添加分隔符,而不用每次都写Array(zip(animals.indices, animals))

struct ForEachDividerView<Data, Content>: View where Data: RandomAccessCollection, Data.Element: Hashable, Content: View {
    var data: Data
    var content: (Data.Element) -> Content

    var body: some View {
        let enumerated = Array(zip(data.indices, data))
        ForEach(enumerated, id: \.1) { index, data in

            /// generate the view
            content(data)

            /// add a divider if the element isn't the last
            if let index = index as? Int, index != enumerated.count - 1 {
                Divider()
                    .background(.blue)
            }
        }
    }
}

/// usage
ForEachDividerView(data: animals) { animal in
    Text(animal)
}

这样做效果很好,隔离了所有的样板zip代码,仍然得到相同的结果,但是,这只是因为animalsString的数组,它符合Hashable-如果数组中的元素不符合Hashable,它就不能工作:

struct Person {
    var name: String
}

struct ContentView: View {
    let people: [Person] = [
        .init(name: "Anna"),
        .init(name: "Bob"),
        .init(name: "Chris")
    ]

    var body: some View {
        VStack {

            /// Error! Generic struct 'ForEachDividerView' requires that 'Person' conform to 'Hashable'
            ForEachDividerView(data: people) { person in
                Text(person.name)
            }
        }
    }
}

这就是为什么SwiftUI的ForEach附带了一个额外的初始化器init(_:id:content:),它接受一个自定义的键路径来提取ID。我想在我的ForEachDividerView中利用这个初始化器,但是我无法理解它。下面是我尝试的:

struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
    var data: Data
    var id: KeyPath<Data.Element, ID>
    var content: (Data.Element) -> Content

    var body: some View {
        let enumerated = Array(zip(data.indices, data))

        /// Error! Invalid component of Swift key path
        ForEach(enumerated, id: \.1.appending(path: id)) { index, data in

            content(data)

            if let index = index as? Int, index != enumerated.count - 1 {
                Divider()
                    .background(.blue)
            }
        }
    }
}

/// at least this part works...
ForEachDividerView(data: people, id: \.name) { person in
    Text(person.name)
}

我尝试使用appending(path:)将第一个密钥路径(从enumerated中提取元素)与第二个密钥路径(从元素中获取Hashable属性)组合在一起,但得到的是Invalid component of Swift key path
如何在ForEach的元素之间自动添加分隔符,即使元素不符合Hashable

nwlqm0z1

nwlqm0z11#

简单方法

struct ContentView: View {
let animals = ["Apple", "Bear", "Cat", "Dog", "Elephant"]

var body: some View {
    VStack {

        ForEach(animals, id: \.self) { animal in
            Text(animal)

            if animals.last != animal  {
                Divider()
                    .background(.blue)
            }
        }
    }
}
}

通常情况下,动物体内的类型必须是可识别的。在这种情况下,代码将修改为。

if animals.last.id != animal.id {...}

这将避免任何等同的要求/实现

yqlxgs2m

yqlxgs2m2#

我使用the article mentioned in a comment构建了以下代码,它获取一组视图并在它们之间放置一个分隔符。
当视图不是由ForEach生成时,这也是有用的,特别是当一个或多个视图被有条件地移除时(例如,使用if语句)。

struct Divided<Content: View>: View {
    var content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        _VariadicView.Tree(DividedLayout()) {
            content
        }
    }

    struct DividedLayout: _VariadicView_MultiViewRoot {
        @ViewBuilder
        func body(children: _VariadicView.Children) -> some View {
            let last = children.last?.id

            ForEach(children) { child in
                child

                if child.id != last {
                    Divider()
                }
            }
        }
    }
}

struct Divided_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            Divided {
                Text("Alpha")
                Text("Beta")
                Text("Gamma")
            }
        }
        .previewDisplayName("Vertical")

        HStack {
            Divided {
                Text("Alpha")
                Text("Beta")
                Text("Gamma")
            }
        }
        .previewDisplayName("Horizontal")
    }
}
velaa5lx

velaa5lx3#

是否所有内容都需要在ForEach中?如果不是,您可以考虑根本不使用索引:

struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
    var data: Data
    var id: KeyPath<Data.Element, ID>
    var content: (Data.Element) -> Content
    
    var body: some View {
        if let first = data.first {
            content(first)
            
            ForEach(data.dropFirst(), id: id) { element in
                Divider()
                    .background(.blue)
                content(element)
            }
        }
    }
}
8wtpewkr

8wtpewkr4#

找到解决办法了!

  1. appending(path:)似乎只对擦除到AnyKeyPath的关键路径起作用。
    1.然后,appending(path:)返回一个可选的AnyKeyPath?-这需要被转换为KeyPath<(Data.Index, Data.Element), ID>以满足id参数。
struct ForEachDividerView<Data, Content, ID>: View where Data: RandomAccessCollection, ID: Hashable, Content: View {
    var data: Data
    var id: KeyPath<Data.Element, ID>
    var content: (Data.Element) -> Content

    var body: some View {
        let enumerated = Array(zip(data.indices, data))

        /// first create a `AnyKeyPath` that extracts the element from `enumerated`
        let elementKeyPath: AnyKeyPath = \(Data.Index, Data.Element).1

        /// then, append the `id` key path to `elementKeyPath` to extract the `Hashable` property
        if let fullKeyPath = elementKeyPath.appending(path: id) as? KeyPath<(Data.Index, Data.Element), ID> {
            ForEach(enumerated, id: fullKeyPath) { index, data in

                content(data)

                if let index = index as? Int, index != enumerated.count - 1 {
                    Divider()
                        .background(.blue)
                }
            }
        }
    }
}

用法:

struct Person {
    var name: String
}

struct ContentView: View {
    let people: [Person] = [
        .init(name: "Anna"),
        .init(name: "Bob"),
        .init(name: "Chris")
    ]

    var body: some View {
        VStack {
            ForEachDividerView(data: people, id: \.name) { person in
                Text(person.name)
            }
        }
    }
}

结果:

相关问题