Подпредставления SwiftUI обрезаются внутри прокрутки

Я создал представление карусели, используя ScrollView и LazyHStack следующим образом:

struct CarouselView<Content: View>: View {
    let content: Content
    @State private var currentIndex = 0
    @State private var contentSize: CGSize = .zero
    
    private let showsIndicators: Bool
    private let spacing: CGFloat
    private let shouldSnap: Bool
    
    init(showsIndicators: Bool = true,
         spacing: CGFloat = .zero,
         shouldSnap: Bool = false,
         @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.showsIndicators = showsIndicators
        self.spacing = spacing
        self.shouldSnap = shouldSnap
    }
    
    var body: some View {
        VStack {
            ScrollView(.horizontal, showsIndicators: showsIndicators) {
                LazyHStack(spacing: spacing) {
                    content
                }.apply {
                    if #available(iOS 17.0, *), shouldSnap {
                        $0.scrollTargetLayout()
                    } else {
                        $0
                    }
                }
            }
            .clipped() // tried to add this to avoid clipping
            .apply {
                if #available(iOS 17.0, *), shouldSnap {
                    $0.scrollTargetBehavior(.viewAligned)
                } else {
                    $0
                }
            }
        }
    }
}

extension View {
    func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}

Затем я использую его следующим образом:

struct ContentView: View {
    let imagesNames = ["img-1", "img-2", "img-3", "img-4"]
    let numberOfLines = [2, 1, 3, 2]
    
    var body: some View {
        
        VStack(spacing: 40) {
            continuousCarousel
        }
        
    }
    
    private var continuousCarousel: some View {
        CarouselView(showsIndicators: true,
                     spacing: 20) {
            ForEach(0 ..< imagesNames.count, id: \.self) { index in
                createImageTile(with: imagesNames[index],
                                height: 70,
                                numberOfLines: numberOfLines[index])
            }
        }
    }
    
    private func createImageTile(with image: String,
                                 height: CGFloat,
                                 numberOfLines: Int) -> some View {
        VStack(spacing: .zero) {
            Image(image)
                .resizable()
                .cornerRadius(30)
                .scaledToFit()
                .aspectRatio(contentMode: .fill)
                .frame(width: 200, height: 100)
                .padding(.bottom, 30)
            
            Text("Headline")
                .bold()
                .padding(.bottom, 10)
            
            ForEach(0 ..< numberOfLines, id: \.self) { _ in
                Text("Some description here")
                    .padding(.bottom, 5)
            }
            
            Spacer() // added to top align content
        }
    }
}

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

Я попытался добавить модификатор clipped к прокрутке, как предложено в этом ответе , но безрезультатно.

Как я могу это решить?

Обновление на основе пятой идеи Бензи Низа.

Эта идея использования скрытого следа практически заставляет все работать.

Просто для примера я храню все свои заголовки и вспомогательный текст в массиве:

let titles = ["Cedele Bakery Kitchen", "Elements Bar and Grill Woolloomooloo", "Osteria di Russo & Russo", "Hopsters Co-op Brewery"]

let supportingText = ["Bangalay Dining, Shoalhaven Heads\n15.5/20", "Modern Australian | Wine bar", "Neptune’s Grotto\n15.5/20.0", "Vegan mapo tofu with shiitake mushrooms and crispy chilli oil\n<30 mins"]

Реализация Carousel не сильно меняет ситуацию. Следует отметить, что последний бит текста в массиве supportingText является самым длинным.

Единственное отличие состоит в том, что я заменил свой imageTile представлением DemoTile, которое должно определить правильную высоту, используя технику посадочного места:

struct DemoTileView: View {
    
    private let imageName: String
    private let imageWidth: CGFloat
    private let imageHeight: CGFloat
    private let title: String
    private let supportingText: String
    private let allTitles: [String]
    private let allSupportingText: [String]
    
    init(imageName: String,
         title: String,
         allTitles: [String],
         supportingText: String,
         allSupportingText: [String],
         imageWidth: CGFloat,
         imageHeight: CGFloat) {
        self.imageName = imageName
        self.title = title
        self.supportingText = supportingText
        self.allTitles = allTitles
        self.allSupportingText = allSupportingText
        self.imageWidth = imageWidth
        self.imageHeight = imageHeight
    }
    
    var body: some View {
        VStack(spacing: .zero) {
            Image(imageName)
                .resizable()
                .scaledToFill()
                .frame(width: imageWidth, height: imageHeight)
                .clipShape(RoundedRectangle(cornerRadius: Constants.Border.cornerRadius))
                .padding(.bottom, Constants.Padding.imageBottom)
            
            textViewFootprint
                .overlay(alignment: .top) {
                    createTextView(title: title, supportingText: supportingText)
                }
            
            Spacer() // added to top align content
        }
        .frame(width: imageWidth)
    }
    
    private var textViewFootprint: some View {
        ZStack {
            ForEach(allTitles.indices, id: \.self) { index in
                let currentTitle = allTitles[index]
                let currentSupportingText = allSupportingText[index]
                createTextView(title: currentTitle, supportingText: currentSupportingText)
            }
        }
        .hidden()
    }
    
    private func createTextView(title: String, supportingText: String) -> some View {
        VStack {
            createTitleView(for: title)
            createSupportingTextView(for: title)
        }
    }
    
    private func createTitleView(for text: String) -> some View {
        // In an HStack to left align the text
        HStack {
            Text(text)
                .bold()
                .padding(.bottom, Constants.Padding.titleBottom)
            
            Spacer()
        }
    }
    
    private func createSupportingTextView(for text: String) -> some View {
        // In an HStack to left align the text
        HStack {
            Text(supportingText)
                .padding(.bottom, Constants.Padding.supportingTextBottom)
            
            Spacer()
        }
    }
    
    struct Constants {
        struct Padding {
            static let imageBottom = 12.0
            static let titleBottom = 4.0
            static let supportingTextBottom = 20.0
        }
        
        struct Border {
            static let cornerRadius = 30.0
        }
    }
}

Затем в карусели я просто делаю это:

CarouselView(spacing: 10) {
    ForEach(0 ..< imagesNames.count, id: \.self) { index in
        DemoTileView(imageName: imagesNames[index],
                     title: titles[index],
                     allTitles: titles,
                     supportingText: supportingText[index],
                     allSupportingText: supportingText,
                     imageWidth: 180,
                     imageHeight: 120)
        .onTapGesture {
            defaultCarouselIndexTapped = index
        }
    }
}

Первые два предмета расположены идеально

Текст последнего элемента обрезается:

Если я дам немного другие размеры, например 240 и 170, то это не будет усекаться:

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я думаю, вам просто нужно изменить модификаторы, применяемые к Image в функции createImageTile:

  • Вы ставите .aspectRatio(contentMode: .fill). Это переопределяет модификатор .scaledToFit(), который стоит перед ним, поэтому он является избыточным.
  • Модификатор .cornerRadius устарел, вместо него рекомендуется использовать .clipShape.
  • При масштабировании до заполнения форму обрезки следует применять после установки размера кадра. Установка формы клипа перед размером кадра может быть способом сделать это при масштабировании по размеру.

Кстати, параметр height, передаваемый этой функции, не используется.

Итак, вот обновленная версия, в которой используется масштабирование до заполнения. Изображения обрезаются до содержащего кадра (200x100) с помощью закругленного прямоугольника.

Image(image)
    .resizable()
//    .cornerRadius(30)
//    .scaledToFit()
//    .aspectRatio(contentMode: .fill)
    .scaledToFill()
    .frame(width: 200, height: 100)
    .clipShape(RoundedRectangle(cornerRadius: 30))
    .padding(.bottom, 30)

Screenshot


РЕДАКТИРОВАТЬ Судя по вашим комментариям, похоже, что вам нужно найти способ убедиться, что LazyHStack резервирует достаточно места для размещения самого высокого предмета. Вы уже задали еще один вопрос по этому поводу и отметили ответ как принятый, поэтому я ожидал, что вы уже нашли решение. Однако, если проблема все еще остается, вы можете попробовать следующие методы:

  1. Установите минимальную высоту на карусели:
continuousCarousel
    .frame(minHeight: 300, alignment: .top) // 👈 ADDED
  1. Позвольте тексту сжаться до нужного размера:
ForEach(0 ..< imagesNames.count, id: \.self) { index in
    createImageTile(
        // ...
    )
    .minimumScaleFactor(0.1) // 👈 ADDED
  1. Зарезервируйте достаточно места для максимального количества текстовых строк.
Text("Some description here")
    .lineLimit(3, reservesSpace: true) // 👈 ADDED
    .padding(.bottom, 5)
  1. Используйте скрытый контент, чтобы зарезервировать место:
ForEach(0 ..< numberOfLines, id: \.self) { _ in
    Text("Some description here")
        .padding(.bottom, 5)
}
ForEach(0 ..< (3-numberOfLines), id: \.self) { _ in // 👈 BLOCK ADDED
    Text("Hidden")
        .padding(.bottom, 5)
        .hidden()
}
  1. Используйте скрытый заполнитель, чтобы определить размер текста, а затем отобразите фактическое содержимое в виде наложения:
VStack(spacing: .zero) {
    Image(image)
        // ...

    textFootprint // 👈 PLACEHOLDER (implement separately)
        .overlay(alignment: .top) {
            VStack(spacing: 0) {
                Text("Headline")
                    // ...
                ForEach(0 ..< numberOfLines, id: \.self) { _ in
                    // ...
                }
            }
        }
    Spacer() // added to top align content
}

Спасибо. По большей части это работает, однако третий элемент с 4 строками по-прежнему обрезается, однако кажется, что он вытеснен за пределы: prnt.sc/wz4RugFRXu-Y - Это можно решить, используя HStack вместо LazyHStack, но для повышения производительности предпочтительнее использовать LazyHStack.

Shawn Frank 01.05.2024 22:55

Вы прокрутили до третьего пункта? Вот где это происходит. Я выделил весь этот код и применил ваши изменения. Отсечение работает нормально для всех, кроме одного с максимальным количеством строк. Я не применяю никаких размеров к карусели, применяется только размер изображения. Я заметил, что если первый элемент имеет максимальную высоту, то карусель имеет правильный размер. Вероятно, потому, что LazyHStack не знает, что позже появится объект большего размера, и поэтому у нас возникла эта проблема. А также два последних модификатора, которые вы упомянули, к чему бы я их применил?

Shawn Frank 02.05.2024 00:34

Спасибо за варианты, попробую. Мне было интересно, можно ли сделать что-то вроде создания всех представлений плиток, чтобы определить требуемую максимальную высоту, и только затем создать карусель и установить минимальную высоту карусели как максимальную высоту всех плиток/элементов?

Shawn Frank 02.05.2024 10:17

Спасибо, что помогли мне до сих пор. Идея 5 с вашими предложениями у меня почти работает. Я обновил свой вопрос тем, что я реализовал на основе вашей идеи, сейчас проблема в том, что текст в некоторые моменты обрезается.

Shawn Frank 02.05.2024 22:25

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

Shawn Frank 02.05.2024 22:55

@ShawnFrank У меня возникнет соблазн избавиться от всех прокладок. Вместо этого попробуйте использовать .frame(maxWidth: .infinity, alignment: .leading) для принудительного выравнивания по левому краю и .frame(maxHeight: .infinity, alignment: .top) для выравнивания по верхнему краю (там, где это действительно необходимо).

Benzy Neez 02.05.2024 23:17

Что ж, мне удалось выполнить выравнивание, используя описанные выше методы, так что это отличный рефакторинг, однако усечение существует. Рад вынести этот вопрос в отдельный вопрос, если вы считаете, что это того стоит.

Shawn Frank 02.05.2024 23:38

Новый вопрос здесь, если у вас есть еще идеи: stackoverflow.com/questions/78423436/…

Shawn Frank 03.05.2024 10:11

Другие вопросы по теме