swiftui-自动在“ foreach”的每个元素之间添加分隔线

发布于 2025-02-06 18:32:00 字数 3974 浏览 5 评论 0 原文

我正在使用 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(Animal.Indices,Animal))的情况下自动添加隔板。到目前为止,这是我到目前为止的:

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 代码,并且仍然获得相同的结果。但是,这仅仅是因为动物字符串 s的数组,它符合 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)
            }
        }
    }
}

hashable“ rel =” nofollow noreferrer“> hashable - 如果我的 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(appending(路径) 将第一个关键路径(从枚举中提取元素)与第二个密钥路径(从元素获取 hashable 属性),但是我得到了Swift密钥路径的无效组件

即使元素不符合 ,我如何在 foreach 的元素之间自动添加一个分隔线?

I'm using a ForEach to display the contents of an array, then manually showing a divider between each element by checking the element index. Here's my code:

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)
                }
            }
        }
    }
}

Result:

Stack of text with dividers in between

This works, but I'd like a way to automatically add dividers everywhere without writing the Array(zip(animals.indices, animals)) every time. Here's what I have so far:

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)
}

This works great, isolating all the boilerplate zip code and still getting the same result. However, this is only because animals is an array of Strings, which conform to Hashable — if the elements in my array didn't conform to Hashable, it wouldn't work:

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)
            }
        }
    }
}

That's why SwiftUI's ForEach comes with an additional initializer, init(_:id:content:), that takes in a custom key path for extracting the ID. I'd like to take advantage of this initializer in my ForEachDividerView, but I can't figure it out. Here's what I tried:

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)
}

I tried using appending(path:) to combine the first key path (which extracts the element from enumerated) with the second key path (which gets the Hashable property from the element), but I got Invalid component of Swift key path.

How can I automatically add a divider in between the elements of a ForEach, even when the element doesn't conform to Hashable?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

习ぎ惯性依靠 2025-02-13 18:32:01

简单的方式

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 {...}

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

Simple way

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)
            }
        }
    }
}
}

Typically the type inside animals must be Identifiable. In which case the code will be modified as.

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

This will avoid any equatable requirements/ implementations

悲歌长辞 2025-02-13 18:32:01

使用评论中提到的文章我构建了以下内容。它采用一组视图,并在它们之间放置一个分隔线。

当视图不是由 foreach 生成的视图时,这也很有用,尤其是当有条件地删除一个或多个视图时(例如,使用语句使用)。

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")
    }
}

Using the article mentioned in a comment I built the following. It takes the set of views and places a divider between them.

This is also useful when the views are not being generated by a ForEach, especially when one or more of the views is removed conditionally (e.g. using an if statement).

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")
    }
}
素衣风尘叹 2025-02-13 18:32:01

一切都需要在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)
            }
        }
    }
}

Does everything need to be in a ForEach? If not, you can consider not using indices at all:

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)
            }
        }
    }
}
多像笑话 2025-02-13 18:32:01

找到了解决方案!

  1. appending(pathending(path:) 似乎仅在删除到
  2. 然后, appending(path:)返回一个可选 anykeypath? - 这需要降低到键>键>键> keypath&lt;(data.index,data.element),id&gt ; 满足 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)
            }
        }
    }
}

结果:

“垂直堆叠的人名称,蓝色sivider在“ 之间”

Found a solution!

  1. appending(path:) seems to only work on key paths erased to AnyKeyPath.
  2. Then, appending(path:) returns an optional AnyKeyPath? — this needs to get cast down to KeyPath<(Data.Index, Data.Element), ID> to satisfy the id parameter.
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)
                }
            }
        }
    }
}

Usage:

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)
            }
        }
    }
}

Result:

Person names stacked vertically, with blue divider in between

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文