fermoya / SwiftUIPager

Native Pager in SwiftUI
MIT License
1.3k stars 172 forks source link

[FEAT] Allow each Page to expand according to its own content width #223

Open robdeans opened 3 years ago

robdeans commented 3 years ago

Is your feature request related to a problem? Please describe. I am implementing a horizontally-scrolling Pager that fills the width of the screen, where unselected Pages are visible on the sides, and where each Page contains Text of varying lengths.

To do this I use the preferredItemSize with the width calculated by (geometryProxy.size.width / 3). This handles a majority of the cases, however when the Text length exceeds a certain amount (or the screen size is small), the display becomes truncated.

Describe the solution you'd like Is there a way in which each Page's width is dynamic (as in, determined by the length of the String provided)?

For example, if the Strings provided are ["All", "Cat", "Lizard", "Dog"], when Cat is selected, All and Lizard would be visible on the sides (possibly trailing off the screen). When Lizard is selected, I would expect its width to appear larger than the other 3-letter animals, but still maintain the balance where the adjacent space is filled with Cat on the left and Dog on the right.

In an extreme case, I would expectHippopotamus to take up nearly the whole screen, but never exceeding the width of the Pager. It's possible that another person's criteria is that the text continues off the screen, but in my case I think size limitations could be handled by modifiers in the .content { } closure (font shrinking, line breaks, etc).

Describe alternatives you've considered From what I can tell the size of the Page is determined by the pageSize variable, which takes into account preferredItemSize and itemAspectRatio. Changing itemAspectRatio (values ranging from 0.1 to 10) did not produce the desired results. preferredItemSize came the closest, but only by setting the constant from 3 to geometryProxy.size.width / 2, which solves truncation but adds a lot of empty space.

Additional context Perhaps there's something I'm missing to allow Pages to expand according to their own content width. If so let me know, as well as if you think this is worth working towards, why or why not. Thank you!

Screen Shot 2021-06-04 at 6 00 32 PM
fermoya commented 3 years ago

Hi @robdeans , thanks for your feedback. Pager is designed so that every page has the same size. pageSize uses preferredItemSize and itemAspectRatio if the former isn't set. If none is set, the pageSize is set to be the same as Pager's. This means that every page has the same space available to fill. How it is used is your to te client. The reason for this is the item size needs to be the same in order to calculate the right translation.

SwiftUI doesn't offer a bounds property as UIKit and therefore the size needs to be explicit here. The solution comes by using preferredItemSize and setting the width of the longest text (plus the icon)

robdeans commented 3 years ago

Thank you @fermoya for the explanation! If I understand correctly, Pager sets equal widths in order to handle the transitions effectively. This is by design to accommodate SwiftUI, which does not provide the width/bounds of Views because of its declarative nature.

I agree that hardcoding the length of the longest Text/Icon component will get the closest result, and although it may leave a bit of empty space the UI is still very clean (plus SwiftUIPager saved hours, probably days of development 🙌 ).

Hypothetically, if there were to be a way to incorporate dynamic widths for each Page based on the content, do you think this would be feasible by extracting the width of each Page, and including the value in the logic for pageSize?

I experimented briefly with the following code and was able to extract the width for each View. Perhaps this value could be mapped to another variable (naturalSize? Naming...) which could be used to determine pageSize should the modifier be selected?

An obstacle I see is that the view would have to be rendered before any sizes are set, and this might not be the best way to pass data given existing structure. But would be glad to hear your thoughts on any potential approach 🙂

struct DemoView: View {
    var textSize: CGSize = .zero

    var body: some View {
        Text("Some long text")
            .background(
                GeometryReader { proxy in
                    Color.clear
                        .preference(
                            key: SizePreferenceKey.self,
                            value: proxy.size
                        )
                }
            )
            .onPreferenceChange(SizePreferenceKey.self) { preferences in
                self.textSize = preferences // This would have to handle mutations, as in the SwiftUIPager extensions
            }
    }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero

    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue()
    }
}
fermoya commented 3 years ago

Hi @robdeans , thanks for the suggestions. Originally, I used this approach to calculate Pager size but that has trouble when inside a ScrollView.

I think a pager component should have all elements have the same size. Think of a book: what's the point of having different page sizes?

bardigolriz commented 3 years ago

Jumping in. To your last question @fermoya, framed as such, then yes it wouldn’t make sense for each page to be inconsistently sized. That said, I’m looking at Pager perhaps less conceptually and more pragmatically. That is, its underlying qualities offer potential for more varied use cases. For example, what brought me to this is a need to create a UI similar to the camera mode picker where you horizontally scroll (snap) between different options. SwiftUIPager almost facilitates the same UI, except either there’s too much whitespace between items to accommodate the longest entry or there’s truncation to avoid excess whitespace. I guess a perfectly reasonable position for you to take is that the library is not intended for this particular use case. I’m hopeful there is a way though. If I’m not putting the Pager inside a ScrollView, will @robdeans’s approach be the workaround?

konrain commented 3 years ago

@fermoya this is actually big deal for a lot of use cases other than the basic pager. I've been attempting to accomplish it for some time now but the ScrollView trouble issue you mentioned is really annoying and its the same with the regular tabview.

Did you figure anything out with your approach so at least i'm heading in the right direction. Anything would be helpful!