SwiftUI表单中的动态行高

时间:2020-06-19 19:56:25

标签: forms layout swiftui

我正在向SwiftUI表单添加控件,以帮助用户输入数据(并约束输入!)。尽管对Forms有很多喜欢的地方,但我发现在此容器之外可以很好地工作的事情在其中进行了非常意外的事情,并且如何补偿这一问题并不总是显而易见的。

计划将数据字段显示为单行。点击该行时,控件将从数据字段后面滑出-该行将需要扩展(高度)以容纳该控件。

我正在使用Swift Playgrounds开发概念证明(或者就我而言是失败的)。想法是使用ZStack,通过叠加视图并为它们提供不同的zIndex并在点击数据字段视图时应用偏移量,从而允许使用漂亮的滑动动画。听起来很简单,但是当ZStack展开时,Form行当然不会展开。

在扩展时调整ZStack的框架会导致填充中发生各种奇怪的变化(至少看起来是这样),可以通过抵消“ top”视图来补偿,但这会导致其他不可预测的行为。指针和想法深表感谢。

导入SwiftUI

struct MyView: View {
    @State var isDisclosed = false

    var body: some View {
        Form { 
            Spacer()

            VStack { 
                ZStack(alignment: .topLeading) {
                    Rectangle()
                        .fill(Color.red)
                        .frame(width: 100, height: 100)
                        .zIndex(1)
                        .onTapGesture { self.isDisclosed.toggle() }

                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: 100, height: 100)
                        .offset(y: isDisclosed ? 50 : 0)
                        .animation(.easeOut)
                }
            }

            Spacer()
        }
    }
}

折叠式堆叠 Collapsed stack

扩展堆栈-视图与相邻行重叠 Expanded stack - view overlaps adjacent row

在扩展时调整ZStack垂直框架时的结果-顶部填充增加 Result when adjusting ZStack vertical frame when expanded - top padding increases

4 个答案:

答案 0 :(得分:3)

这可能是行高变化灵活的解决方案(使用SwiftUI - Animations triggered inside a View that's in a list doesn't animate the list as well中的解决方案中使用的AnimatingCellHeight修饰符。

通过Xcode 11.4 / iOS 13.4测试

demo

struct MyView: View {
    @State var isDisclosed = false

    var body: some View {
        Form {
            Spacer()

            ZStack(alignment: .topLeading) {
                Rectangle()
                    .fill(Color.red)
                    .frame(width: 100, height: 100)
                    .zIndex(1)
                    .onTapGesture { withAnimation { self.isDisclosed.toggle() } }

                HStack {
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: 100, height: 100)
                }.frame(maxHeight: .infinity, alignment: .bottom)
            }
            .modifier(AnimatingCellHeight(height: isDisclosed ? 150 : 100))

            Spacer()
        }
    }
}

答案 1 :(得分:1)

使用alignmentGuide代替offset

...
//.offset(y: isDisclosed ? 50 : 0)
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 50 : 0) })
...

offset不会影响其视图的框架。这就是为什么Form无法按预期做出反应的原因。相反,alignmentGuide确实如此。

答案 2 :(得分:0)

感谢Kyokook(感谢我直接使用offset())和Asperi

我认为Kyokook的解决方案(使用AlignmentGuides)更简单,并且我会偏爱它,因为它利用了Apple现有的API,并且似乎导致容器中视图的移动更加不可预测。但是,行高突然改变并且不同步。 Asperi的示例中的动画更平滑,但行中的视图有些跳动(这几乎就像是填充或嵌入在更改,然后在动画结束时重置)。我制作动画的方法有点失败,因此欢迎提出任何进一步的评论。

解决方案1(帧保持一致,动画不稳定):

struct ContentView: View {
@State var isDisclosed = false

var body: some View {
    Form {
        Text("Row 1")
        
        VStack {
            ZStack(alignment: .topLeading) {
                Rectangle()
                    .fill(Color.red)
                    .frame(width: 100, height: 100)
                    .zIndex(1)
                    .onTapGesture {
                        self.isDisclosed.toggle()
                }
                
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                    .alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 100 : 0) })
                    .animation(.easeOut)
                
                
                Text("Row 3")
            }
        }
        
        Text("Row 3")
        
    }
}
}

solution 1

解决方案2(平滑动画,但帧差异):

struct ContentView: View {
    @State var isDisclosed = false
    
    var body: some View {
        Form {
            Text("Row 1")
            
            VStack {
                ZStack(alignment: .topLeading) {
                    Rectangle()
                        .fill(Color.red)
                        .frame(width: 100, height: 100)
                        .zIndex(1)
                        .onTapGesture {
                            withAnimation { self.isDisclosed.toggle() }
                    }
                    

                    HStack {
                        Rectangle()
                            .fill(Color.blue)
                            .frame(width: 100, height: 100)
                    }.frame(maxHeight: .infinity, alignment: .bottom)
                }
                .modifier(AnimatingCellHeight(height: isDisclosed ? 200 : 100))
            }
            
            Text("Row 3")
        }
    }
}

struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0

var animatableData: CGFloat {
    get { height }
    set { height = newValue }
}

func body(content: Content) -> some View {
    content.frame(height: height)
}

}

solution 2

答案 3 :(得分:0)

我现在有一个使用Kyokook建议的对齐指南的有效实现。我通过在步进器滑出时向其添加不透明度动画来缓和行高变化。这也有助于防止在关闭控件时行标题出现轻微的重叠。

struct ContentView: View {
// MARK: Logic state
@State private var years = 0
@State private var months = 0
@State private var weeks = 0

// MARK: UI state
@State var isStepperVisible = false

var body: some View {
    Form {
        Text("Row 1")
        
        VStack {
            // alignment guide must be explicit for the ZStack & all child ZStacks
            // must use the same alignment guide - weird stuff happens otherwise
            ZStack(alignment: .top) {
                HStack {
                    Text("AGE")
                        .bold()
                        .font(.footnote)
                    
                    Spacer()
                    
                    Text("\(years) years \(months) months \(weeks) weeks")
                        .foregroundColor(self.isStepperVisible ? Color.blue : Color.gray)
                }
                .frame(height: 35) // TODO: Without this, text in HStack vertically offset. Investigate. (HStack align doesn't help)
                .background(Color.white) // Prevents overlap of text during transition
                .zIndex(3)
                .contentShape(Rectangle())
                .onTapGesture {
                        self.isStepperVisible.toggle()
                }
                
                
                HStack(alignment: .center) {
                    StepperComponent(value: $years, label: "Years", bounds: 0...30, isVisible: $isStepperVisible)
                    StepperComponent(value: $months, label: "Months", bounds: 0...12, isVisible: $isStepperVisible)
                    StepperComponent(value: $weeks, label: "Weeks", bounds: 0...4, isVisible: $isStepperVisible)
                }
                .alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isStepperVisible ? 40 : 0) })
            }
        }
        
        Text("Row 3")
        
    }
}
}

struct StepperComponent<V: Strideable>: View {
// MARK: Logic state
@Binding var value: V
var label: String
var bounds: ClosedRange<V>
//MARK: UI state
@Binding var isVisible: Bool

var body: some View {
    ZStack(alignment: .top) {
        Text(label.uppercased()).font(.caption).bold()
            .frame(alignment: .center)
            .zIndex(1)
            .opacity(self.isVisible ? 1 : 0)
            .animation(.easeOut)
        
        Stepper(label, value: self.$value, in: bounds)
            .labelsHidden()
            .alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isVisible ? 25 : 0) })
            .frame(alignment: .center)
            .zIndex(2)
            .opacity(self.isVisible ? 1 : 0)
            .animation(.easeOut)
    }
    
}
}

这里仍然有一些改进的余地,但是总的来说,我对结果感到满意:-)

canvas preview