Dynami c высота строки в форме SwiftUI - PullRequest
2 голосов
/ 19 июня 2020

Я добавляю элементы управления в форму SwiftUI, чтобы помочь пользователю вводить данные (и ограничивать ввод!). Хотя в Forms есть что нравится, я обнаружил, что вещи, которые прекрасно работают вне этого контейнера, делают внутри него очень неожиданные вещи, и не всегда очевидно, как это компенсировать.

План состоит в том, чтобы иметь поле данных отображается в виде одной строки. При нажатии на строку элемент управления выдвигается из-за поля данных - строку необходимо будет расширить (по высоте), чтобы разместить элемент управления.

Я использую Swift Playgrounds для разработки доказательства концепции (или неудача в моем случае). Идея состоит в том, чтобы использовать ZStack, который позволит создать красивую скользящую анимацию, перекрывая представления и задавая им другой zIndex и применяя смещение при касании представления поля данных. Звучит просто, но, конечно, строка формы не расширяется при расширении ZStack.

Регулировка рамки ZStack при расширении вызывает всевозможные странные изменения в отступах (или, по крайней мере, это похоже на это), которые можно компенсировать встречным смещением «верхнего» вида, но это вызывает другие непредсказуемые поведение. Указатели и идеи с благодарностью приняты.

import 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 ]

3 голосов
/ 20 июня 2020

Вот возможное решение с плавным изменением высоты строки (с использованием модификатора AnimatingCellHeight, взятого из моего решения в SwiftUI - анимация, запускаемая внутри представления, которое находится в списке, также не анимирует список ).

Протестировано с 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 голос
/ 20 июня 2020

Используйте alignmentGuide вместо offset.

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

offset не влияет на рамку его представления. вот почему форма не реагирует должным образом. Напротив, alignmentGuide делает.

0 голосов
/ 21 июня 2020

Теперь у меня есть рабочая реализация с использованием направляющих по выравниванию, как это было предложено Kyokook . Я смягчил несколько резкое изменение высоты строки, добавив анимацию непрозрачности для Stepper, когда он выдвигается. Это также помогает предотвратить легкое перекрытие заголовка строки при закрытии элемента управления.

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

Здесь все еще есть возможности для улучшения, но в целом я доволен результатом :-)

предварительный просмотр холста

0 голосов
/ 20 июня 2020

Спасибо обоим Kyokook (за то, что поставили меня прямо на offset ()) и Asperi .

Я думаю, что решение Kyokook (с использованием AlignmentGuides) проще и Я бы предпочел, чтобы он использовал существующий API Apple и, кажется, вызывает менее непредсказуемое перемещение представлений в их контейнере. Однако высота строки резко меняется и не синхронизируется. Анимация в примере с 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

Solution 2 (smoother animation but frame variance):

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

}

решение 2

...