apptekstudios / ASCollectionView

A SwiftUI collection view with support for custom layouts, preloading, and more.
MIT License
1.36k stars 160 forks source link

dynamic height in horizontally scrollable section #186

Open seboslaw opened 4 years ago

seboslaw commented 4 years ago

Hey guys,

I'm basically trying to implement the AppStore example (vertical scroll view with horizontally scrollable sections) but with dynamic type support. In order for this to work I've modified the var layout: ASCollectionLayout<Int> extension to deliver the following code:

            default:
                return ASCollectionLayoutSection
                { environment in                    
                    let itemSize = NSCollectionLayoutSize(
                        widthDimension: .fractionalWidth(1),
                        heightDimension: .estimated(350))

                    let item = NSCollectionLayoutItem(layoutSize: itemSize)

                    let groupSize = NSCollectionLayoutSize(
                        widthDimension: .fractionalWidth(1),
                        heightDimension: .estimated(350))

                    let group = NSCollectionLayoutGroup.horizontal(
                        layoutSize: groupSize, 
                        subitems: [item])

                    let headerSize = NSCollectionLayoutSize(
                        widthDimension: .fractionalWidth(1.0),
                        heightDimension: .estimated(44))

                    let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
                        layoutSize: headerSize,
                        elementKind: UICollectionView.elementKindSectionHeader, 
                        alignment: .top)

                    let section = NSCollectionLayoutSection(group: group)
                    section.boundarySupplementaryItems = [sectionHeader]
                    section.orthogonalScrollingBehavior = .continuous
                    section.interGroupSpacing = 12

                    return section

However, the .estimated(350) for the item's and group's height don't seem to work. The resulting section always has a height of 350 and doesn't adopt to the actual content's height. The actual cell content has a blue background for debugging purposes - when you scroll sideways you get the next item...in the next release these cells should only be 1/3 of the screen's width. The white area underneath the blue cell however should actually shrink.

Screenshot 2020-10-04 at 17 59 05

Cheers, Sebastian

apptekstudios commented 3 years ago

Apple's UICollectionViewCompositionalLayout (which is what you're using here) seems to struggle with this type of sizing. You'll notice that where they use it on the app store and elsewhere they actually use a fixed size. Curiously, none of their demos show the estimated sizing being used either. I'm not sure how to fix this other than to use a fixed size sorry!

andrei1152 commented 3 years ago

I think I've solved this, only be mere chance and with a lot of extra additional help.

image

There's a lot of things that I've done to make this work, and I don't know which of them will help you.

This is how I'm using ASCollectionView to create a horizontally scrollable list of cards. The .dynamicHorizontalList function is similar to what you've used for layout, in that it allows you to set values for height as well, instead of .fractional(1) value offered with the .list() method

ASCollectionView(data: stores) {store,_ in
            StoreCard(store: store)
        }.layout(scrollDirection: .horizontal) { sectionId in
            .dynamicHorizontalList(itemWidth: .fractionalWidth(0.42), itemHeight: .estimated(200), spacing: 10, sectionInsets: .init(top: 0, leading: AppSpacings.large, bottom: 0, trailing: 0))
        }
        .scrollIndicatorsEnabled(horizontal: false)
        .fitContentSize(dimension: .vertical)
        .padding(.bottom, AppSpacings.large)
        .listRowInsets(EdgeInsets())

Another thing to note is that I've used a custom HorizontalGeometryReader to compute the height of the cell, which only takes the width value of the parent and does not scale on the y-axis like the usual GeometryReader.

var body: some View {
        HorizontalGeometryReader { width in
            VStack(alignment: .leading) {
                ZStack(alignment: .bottom) {
                    KFImage(store.storeImageURL)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: width, height: width * 1.4)
                        .cornerRadius(20)
                        .clipped()
                    KFImage(store.storeLogoIconURL)
                        .resizable()
                        .frame(width: 80, height: 80)
                        .aspectRatio(contentMode: .fill)
                        .clipShape(Circle())
                        .offset(y: -AppSpacings.small)
                }
                HStack {
                    Image("time")
                    Text(estimatedTime)
                    Spacer()
                    Image("car")
                    Text(shippmentPrice)
                }
                    .font(.regularSmall)
                    .accentColor(.appPrimary)

                Text(store.storeName).font(.semiboldMedium)
                Spacer()
            }
        }
    }

The final sauce that made this work, which is something that I've discovered by accident, was adding the Spacer() in the VStack that I've used for the card in the code above. Without this, the card will not grow to fit its content, but will instead collapse along its y-axis. I don't understand why, but oh well.

Also, here's the code for the HorizontalGeometryReader class.

import SwiftUI

struct WidthReader : PreferenceKey, Equatable {
    static var defaultValue: CGFloat { 10 }

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
    }
}

struct WidthReaderView : View {
    var body: some View {
        GeometryReader { geometry in
            Color.clear.preference(key: WidthReader.self, value: geometry.size.width)
        }
    }
}

public struct HorizontalGeometryReader<Content: View> : View {

    var content: (CGFloat)->Content
    @State private var width: CGFloat = WidthReader.defaultValue

    public init(@ViewBuilder content: @escaping (CGFloat)->Content) {
        self.content = content
    }

    public var body: some View {
        content(width)
            .frame(minWidth: 0, maxWidth: .infinity)
            .background(WidthReaderView())
            .onPreferenceChange(WidthReader.self) { width in
                self.width = width
            }
    }
}