Я создал представление карусели, используя 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, то это не будет усекаться:





Я думаю, вам просто нужно изменить модификаторы, применяемые к 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)

РЕДАКТИРОВАТЬ Судя по вашим комментариям, похоже, что вам нужно найти способ убедиться, что LazyHStack резервирует достаточно места для размещения самого высокого предмета. Вы уже задали еще один вопрос по этому поводу и отметили ответ как принятый, поэтому я ожидал, что вы уже нашли решение. Однако, если проблема все еще остается, вы можете попробовать следующие методы:
continuousCarousel
.frame(minHeight: 300, alignment: .top) // 👈 ADDED
ForEach(0 ..< imagesNames.count, id: \.self) { index in
createImageTile(
// ...
)
.minimumScaleFactor(0.1) // 👈 ADDED
Text("Some description here")
.lineLimit(3, reservesSpace: true) // 👈 ADDED
.padding(.bottom, 5)
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()
}
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
}
Вы прокрутили до третьего пункта? Вот где это происходит. Я выделил весь этот код и применил ваши изменения. Отсечение работает нормально для всех, кроме одного с максимальным количеством строк. Я не применяю никаких размеров к карусели, применяется только размер изображения. Я заметил, что если первый элемент имеет максимальную высоту, то карусель имеет правильный размер. Вероятно, потому, что LazyHStack не знает, что позже появится объект большего размера, и поэтому у нас возникла эта проблема. А также два последних модификатора, которые вы упомянули, к чему бы я их применил?
Спасибо за варианты, попробую. Мне было интересно, можно ли сделать что-то вроде создания всех представлений плиток, чтобы определить требуемую максимальную высоту, и только затем создать карусель и установить минимальную высоту карусели как максимальную высоту всех плиток/элементов?
Спасибо, что помогли мне до сих пор. Идея 5 с вашими предложениями у меня почти работает. Я обновил свой вопрос тем, что я реализовал на основе вашей идеи, сейчас проблема в том, что текст в некоторые моменты обрезается.
так что это исправляет текст, и он больше не усекается, однако теперь кажется, что плитка выталкивается за пределы, а изображение теперь усекается/вытесняется за пределы, аналогично исходной проблеме, которая у нас была, но только для этой конкретной плитки/элемента .
@ShawnFrank У меня возникнет соблазн избавиться от всех прокладок. Вместо этого попробуйте использовать .frame(maxWidth: .infinity, alignment: .leading) для принудительного выравнивания по левому краю и .frame(maxHeight: .infinity, alignment: .top) для выравнивания по верхнему краю (там, где это действительно необходимо).
Что ж, мне удалось выполнить выравнивание, используя описанные выше методы, так что это отличный рефакторинг, однако усечение существует. Рад вынести этот вопрос в отдельный вопрос, если вы считаете, что это того стоит.
Новый вопрос здесь, если у вас есть еще идеи: stackoverflow.com/questions/78423436/…
Спасибо. По большей части это работает, однако третий элемент с 4 строками по-прежнему обрезается, однако кажется, что он вытеснен за пределы: prnt.sc/wz4RugFRXu-Y - Это можно решить, используя HStack вместо LazyHStack, но для повышения производительности предпочтительнее использовать LazyHStack.