如何使用宽度的框架修饰符使多列列表中的 1 列具有相同的宽度 w/out 以保持灵活性

How to have 1 column in a multiple column list be of the same width w/out using a frame modifier of width so to retain flexibility

我有一个条目列表,其中包含 UI 的多列,除了第一列之外,所有条目的大小都是唯一的水平(即它们是short/long 作为他们的内容要求)。我知道第一个大小一致的列我可以设置框架修改器宽度来实现这一点,但我希望有更好、更灵活的方法来获得所需的行为。原因是我不认为该解决方案经过优化以考虑用户的显示大小或列的实际最大内容宽度。也就是说,当显示尺寸设置为最大时,宽度集要么不够宽,要么,如果是,那么在 smaller/regular 显示尺寸上它会不必要地宽。

这是我目前最好的尝试:

        GeometryReader { geometry in
            
        VStack {
        
            HStack {
                HStack {
                    Text("9am")
                    
                    Image(systemName: "cloud.drizzle").font(Font.title2)
                        .offset(y: 4)
                }.padding(.all)
                .background(Color.blue.opacity(0.2))
                .cornerRadius(16)
                            
                VStack {
                    HStack {
                        Text("Summary")
                            .padding(.trailing, 4)
                            .background(Color.white)
                            .layoutPriority(1)
                        VStack {
                            Spacer()
                            Divider()
                            Spacer()
                        }
                        VStack {
                            Text("12°")
                            Text("25%")
                                .foregroundColor(Color.black)
                                .background(Color.white)
                        }.offset(y: -6)
                        Spacer()
                    }.frame(width: geometry.size.width/1.5)
                }
                
                Spacer()
            }
            
            HStack {
                HStack {
                    Text("10am")
                        .customFont(.subheadline)
                    
                    Image(systemName: "cloud.drizzle").font(Font.title2)
                        .offset(y: 4)
                        .opacity(0)
                }
                .padding(.horizontal)
                .padding(.vertical,4)
                .background(Color.blue.opacity(0.2))
                .cornerRadius(16)
                            
                VStack {
                    HStack {
                        ZStack {
                            Text("Mostly cloudy")
                                .customFont(.body)
                                .padding(.trailing, 4)
                                .background(Color.white)
                                .opacity(0)
                            VStack {
                                Spacer()
                                Divider()
                                Spacer()
                            }
                        }
                        VStack {
                            Text("13°")
                            Text("25%")
                                .foregroundColor(Color.black)
                                .background(Color.white)
                        }.offset(y: -6)
                        Spacer()
                    }.frame(width: geometry.size.width/1.75)
                }
                
                Spacer()
            
            } 
      } 
}

对我来说,这看起来像:

如您所知,上午 10 点比上午 9 点稍宽。为了使它们的大小尽可能接近,我也在其中添加了一个云图标,尽管不透明度为零。理想情况下,上午 10 点的大小与上午 9 点的大小相同,不需要透明的云图标。更一般地说,有意义的是识别该列中最宽的 HStack,然后无论其宽度是多少,都将应用于所有其他列。请记住,出于演示目的,我上面的代码是静态的。这将是一个通过行集合进行迭代呈现的视图。

您可以使用动态框架修改器,例如 frame(.maxWidth: .infinity) 修改器来扩展视图,以便它们填满整个框架,即使框架是动态的。这是一个可以帮助您开始的示例:

struct CustomContent: View {
    
    var body: some View {
        VStack {
            VStack {
                CustomRow(timeText: "9am", systemIcon: "cloud.drizzle", centerText: "Summary", temperature: "12°", percent: "25%")
                CustomRow(timeText: "10am", systemIcon: nil, centerText: nil, temperature: "13°", percent: "25%")
            }
            
            VStack {
                CustomRow(timeText: "9am", systemIcon: "cloud.drizzle", centerText: "Summary", temperature: "12°", percent: "25%")
                CustomRow(timeText: "10am", systemIcon: nil, centerText: nil, temperature: "13°", percent: "25%")
            }
            .frame(width: 300)
            
        }
    }
}

struct CustomContent_Previews: PreviewProvider {
    static var previews: some View {
        CustomContent()
    }
}

struct CustomRow: View {
    
    let timeText: String
    let systemIcon: String?
    let centerText: String?
    let temperature: String
    let percent: String
    
    var body: some View {
        HStack {
            
            //Left column
            HStack(alignment: .center) {
                Text(timeText)
                
                if let icon = systemIcon {
                    Image(systemName: icon)
                        .font(.title2)
                }
            }
            .padding(.all)
            .frame(width: 105, height: 60)
            .background(Color.blue.opacity(0.2))
            .cornerRadius(16)

            // Center column
            ZStack(alignment: .leading) {
                Capsule()
                    .fill(Color.black.opacity(0.3))
                    .frame(height: 0.5)
                if let text = centerText {
                    Text(text)
                        .lineLimit(1)
                        .background(Color.white)
                }
            }
            
            // Right column
            VStack {
                Text(temperature)
                Text(percent)
                    .foregroundColor(Color.black)
            }
            
        }
    }
}

https://www.wooji-juice.com/blog/stupid-swiftui-tricks-equal-sizes.html 的指导下,我完成了这个。

这是 UI 的一部分,我想确保所有行的水平大小均等,宽度设置为最高值:

            HStack {
                VStack {
                    Spacer()
                    Text("9am")
                    Spacer()
                }
            }.frame(minWidth: self.maximumSubViewWidth)
            .overlay(DetermineWidth())

上面包含的堆栈有一个 OnPreferenceChange 修饰符:

           .onPreferenceChange(DetermineWidth.Key.self) {
                if [=11=] > maximumSubViewWidth {
                    maximumSubViewWidth = [=11=]
                }
            }

奇迹发生在这里:

struct MaximumWidthPreferenceKey: PreferenceKey
{
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat)
    {
        value = max(value, nextValue())
    }
}

struct DetermineWidth: View
{
    typealias Key = MaximumWidthPreferenceKey
    var body: some View
    {
        GeometryReader
        {
            proxy in
            Color.clear
                .anchorPreference(key: Key.self, value: .bounds)
                {
                    anchor in proxy[anchor].size.width
                }
        }
    }
}

顶部的 link 最准确地描述了每个人的用途。

MaximumWidthPreferenceKey

This defines a new key, sets the default to zero, and as new values get added, takes the widest

确定宽度

This view is just an empty (Color.clear) background, but with our new preference set to its width. We’ll get back to that clear background part in a moment, but first: there are several ways to set preferences, here, we’re using anchorPreference. Why?

Well, anchorPreference has “No Overview Available” so I don’t actually have a good answer for that, other than it seems to be more reliable in practice. Yeah, cargo-cult code. Whee! I have a hunch that, what with it taking a block and all, SwiftUI can re-run that block to get an updated value when there are changes that affect layout.

Another hope I have is that this stuff will get better documented, so that we can better understand how these different types are intended to be used and new SwiftUI developers can get on board without spending all their time on Stack Overflow or reading blog posts like this one.

Anyway, an anchor is a token that represents a dimension or location in a view, but it doesn’t give you the value directly, you have to cash it in with a GeometryProxy to get the actual value, so, that’s what we did — to get the value, you subscript a proxy with it, so proxy[anchor].size.width gets us what we want, when anchor is .bounds (which is the value we passed in to the anchorPreference call). It’s kind of twisted, but it gets the job done.

maximumSubViewWidth 是从父视图传入的绑定变量,以确保每个子视图引用的 maximumSubViewWidth 始终是最新的最大值。

            ForEach(self.items) { item, in
                ItemSubview(maximumSubViewWidth: $maximumSubViewWidth, item: item)
            }

唯一的问题是整行有一个不希望出现的细微但仍然很明显的动画,任何 UI 的大小都调整为最大宽度。我为解决此问题所做的工作是向父容器添加一个动画修饰符,该修饰符从 nil 开始,在显式触发后切换回 .default。

.animation(self.initialised ? .default : nil)

我在用户明确与该行交互后将 self.initialised 设置为 true(在我的例子中,他们点击一行以展开以显示其他信息)——这确保了初始动画不会错误发生,但之后动画恢复正常。我最初的尝试是在 .onAppear 修饰符中切换初始化的状态,以便更改是自动的,但这没有用,因为我假设调整大小可以在初始外观之后发生。

另一件需要注意的事情(这可能表明虽然这个解决方案有效,但它不是最好的方法)是我在控制台中看到这条消息重复出现在每一个项目上,或者只是那些需要调整大小(不清楚,但警告总数 = 项目数):

Bound preference MaximumWidthPreferenceKey tried to update multiple times per frame.

如果有人能想出一种方法来实现上述目标,同时避免此警告,那就太好了!

更新:我想通了以上内容。

这实际上是一个重要的变化,因为在没有解决这个问题的情况下,我看到该列在随后访问屏幕时变得越来越宽。

视图有一个新的 widthDetermined @State 变量,该变量设置为 false,并在 .onAppeared 中变为 true。

然后,如果 widthDetermined 为 false,即未设置,我仅确定视图的宽度。我通过使用 https://fivestars.blog/swiftui/conditional-modifiers.html:

中提出的条件修饰符来做到这一点
    func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> TupleView<(Self?, Content?)> {
    if conditional { return TupleView((nil, content(self))) }
    else { return TupleView((self, nil)) }
}

并且在视图中:

            .if(!self.widthDetermined) {
                [=16=].overlay(DetermineWidth())
            }