SwiftUI ScrollView VStack GeometryReader высота игнорируется

Oct 10 2020

Я хочу использовать ScrollView вне VStack, чтобы мой контент можно было прокручивать, если VStack выходит за пределы размера экрана. Теперь я хочу использовать его GeometryReaderв VStack, и это вызывает проблемы, которые я могу решить, только установив фрейм GeometryReader, что мне не очень помогает, учитывая, что я использую средство чтения для определения размера представления.

Вот код без ScrollView, и он отлично работает:

struct MyExampleView: View {
  var body: some View {
    VStack {
        Text("Top Label")
            .background(Color.red)
        
        GeometryReader { reader in
            Text("Custom Sized Label")
                .frame(width: reader.size.width, height: reader.size.width * 0.5)
                .background(Color.green)
        }
        
        Text("Bottom Label")
            .background(Color.blue)
    }
    .background(Color.yellow)
  }
}

В результате получается следующее изображение:

Этикетка нестандартного размера должна быть полной ширины, но половина ширины - высоты. Теперь, если я заключу тот же код в ScrollView, произойдет следующее:

Мало того, что все стало меньше, но и высота ярлыка нестандартного размера как-то игнорируется. Если я установлю высоту GeometryReader, я могу настроить это поведение, но я хочу, чтобы GeometryReader увеличивался до размера его содержимого. Как я могу этого добиться?

благодаря

Ответы

1 Asperi Oct 10 2020 at 20:02

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

Вот возможное решение для вашего сценария - подходящий инструмент здесь - это настройки просмотра. Подготовлено и протестировано с Xcode 12 / iOS 14.

struct DemoLayout_Previews: PreviewProvider {
    static var previews: some View {
        Group {
          MyExampleView()
          ScrollView { MyExampleView() }
        }
    }
}

struct MyExampleView: View {
    @State private var height = CGFloat.zero
    var body: some View {
        VStack {
            Text("Top Label")
                .background(Color.red)
            
            Text("Custom Sized Label")
                .frame(maxWidth: .infinity)
                .background(GeometryReader {
                    // store half of current width (which is screen-wide)
                    // in preference
                    Color.clear
                        .preference(key: ViewHeightKey.self, 
                            value: $0.frame(in: .local).size.width / 2.0) }) .onPreferenceChange(ViewHeightKey.self) { // read value from preference in state self.height = $0
                }
                .frame(height: height) // apply from stored state
                .background(Color.green)
            
            Text("Bottom Label")
                .background(Color.blue)
        }
        .background(Color.yellow)
    }
}

struct ViewHeightKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

Примечание: ... и не используйте GeometryReader, если вы не уверены в контексте, в котором находится ваше представление.