fermoya / SwiftUIPager

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

[FEAT] Support RandomAccessCollection in the initializers rather than fixed Array #135

Closed TheNoim closed 4 years ago

TheNoim commented 4 years ago

Is your feature request related to a problem? Please describe. There is currently no good way library out that supports infinitive page swiping with an unknown size. This could be very useful. Example use case: Infinitiv calendar swipe view. The issue is, a calendar does not have a clear end in both directions. First, when I found this library I thought I finally have a good way to implement a week calendar view. However, then I found out that I still need to pass an array with a known size and a positive index.

Describe the solution you'd like A way to use this without passing an array. Instead of:

@State var page: Int = 0
var items = Array(0..<10)

var body: some View {
    Pager(page: $page,
          data: items,
          id: \.identifier,
          content: { index in
              // create a page based on the data passed
              Text("Page: \(index)")
     })
 }

this

@State var page: Int = 0

var body: some View {
    Pager(page: $page,
          content: { index in
              // create a page based on the data passed
              // Index goes in both directions and can be negative
              Text("Page: \(index)")
     })
 }

Describe alternatives you've considered

The alternative would be to allow to pass custom array implementations. For example: Some struct which implements the RandomAccessCollection. Then you could implement your own "array" which auto extends itself in both directions. Negative and positive. RandomAccessCollection supports this:

    override var startIndex: Self.Index { get }

    override var endIndex: Self.Index { get }

You can update and define your own end and start.

I tried to implement something similar once by myself. But this is a bit buggy and not really nice: https://gist.github.com/TheNoim/6ca6f35eb830a2d53d6adf04d68e05ba

fermoya commented 4 years ago

@TheNoim I'm a bit confused by "A way to use this without passing an array". How is Pager supposed to know what populates the pages, otherwise? What you're seeing as an argument of the view builder is not an index but a data item. This is needed to populate the page.

All in all, the way you describe Pager behavior is incorrect. In the example provided, which I think you've got from the documentation and I'm gonna rename, index it's not just a random integer but a data item. So some might want to page integers from 0 to 10, some other users might want to iterate them from 10 to 20, some others might want to iterate months as you want. This is why "A way to use this without passing an array" doesn't quite makes sense. Pager takes an data array, a binding and a view builder block. The array is used to build the page at a specific index (that is, next/previous pages). The binding allows you to keep track of the current index.

Just for further clarification:

@State var page: Int = 0
var items = Array(0..<10)

var body: some View {
    Pager(page: $page,
          data: items,
          id: \.identifier,
          content: { index in
              //
              // This isn't just an index. It's an item from data. In this example is an integer
              //
              Text("Page: \(index)")
     })
 }

What you would need to do is something like this:

// 9 as we're in October
@State var page: Int = CalendaryMonth.current.monthIndex
@State var oldPage: Int = CalendaryMonth.current.monthIndex
@State var year = 2020
let months = [CalendarMonth(.january), ..., CalendarMonth(.october), CalendarMonth(.november), CalendarMonth(.december)]

var body: some View {
    Pager(page: $page,
          data: months,
          id: \.monthIndex,
          content: { month in
              // Render month accordingly depending on the year
              CalendarViewMonth(month: month, year: year)
     })
     .loopPages()
     .onPageChanged { newPage in
         if newPage == 0 && page == 11 { year += 1 }
         if newPage == 11 && oldPage == 0 { year -= 1 }
         oldPage = newPage
    }
 }
TheNoim commented 4 years ago

Pager takes an data array, a binding and a view builder block. The array is used to build the page at a specific index (that is, next/previous pages). The binding allows you to keep track of the current index.

This is not really against my post. My suggestion was more like this: Allow to pass an object conforming to the RandomAccessCollection protocol. This would allow to define your own end and start of Indicies and even updating them dynamically. Currently, you can only pass fixed Data with the index start of zero and a known index end. It would be much cooler if the pager gets no data and the content gets rendered completely dynamic with a passed index. For an infinitive scenario, the pager doesn't need to know the length of an array. It only needs to render the last, the current and next item. It doesn't really need to know what kind of data it is or how many items the data source contains.

fermoya commented 4 years ago

I'm not following, could you please show me an example? Even just pseudo-code. I wanna see how you dynamically add data and change the index.

As far as I know, I think you could have a @State variable for you data and change it dynamically. This is what I do in the example code, InfiniteExampleView.swift, where I append more items on the fly. If going backwards, you could detect that (the way I was doing for year in my previous snippet) and insert more items in your array while incrementing the index (so if you append 5 previous months, then your new index is your current index plus 5).

fermoya commented 4 years ago

I'm gonna start supporting RandomAccessCollection with Int indexes on version 1.13.0. I do the conversion to an Array internally. See version 1.13.0-beta.1

As far as I can see in your gist, this would be enough for you. Then, if you implement onPageChanged, you could call your updateCurrentIndex in this callback:

@State var page = 0 // set here the start index
@State var data: MyRandomAccessCollectionImplementation

Pager(page: $page, data: data, id: \.id) {
        // return your View here
    }
    .onPageChanged { index in
        data.updateCurrentIndex(index)
    }