fermoya / SwiftUIPager

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

[FEAT] Respond to moving the digital crown on the Apple Watch #265

Closed andriy-appuchino closed 2 years ago

andriy-appuchino commented 2 years ago

Hello. Great job! But I ran into a problem that the pager does not respond to moving the digital crown on the Apple Watch, as this usually works with ScrollView. Can I somehow enable digital crown support?

fermoya commented 2 years ago

Hi @andriy-appuchino. I don't think I ever considered the Digital Crown but it should be simple enough. I'll take a look at it next week

andriy-appuchino commented 2 years ago

@fermoya I haven't been able to find a solution, so I'll be waiting to see if you manage to implement it. Thanks a lot.

andriy-appuchino commented 2 years ago

By the way, I just tried to do something myself, I managed to get it to work, but it works a little clunky, not like the native scroll on watchOS.

    @StateObject var page: Page = .first()
    var items = Array(0..<10)
    @State var scrollAmount = 0.0

    var body: some View {
        Pager(page: page,
                  data: items,
                  id: \.self,
                  content: { index in
                      // create a page based on the data passed
                      Text("Page: \(index)")
             })
        .vertical()
        .focusable(true)
        .digitalCrownRotation($scrollAmount, from: 0, through: 10, by: 1, sensitivity: .low, isContinuous: false, isHapticFeedbackEnabled: true)
        .onChange(of: scrollAmount) { newValue in
            withAnimation {
                page.update(.new(index: Int(newValue)))
            }
        }
    }
fermoya commented 2 years ago

@andriy-appuchino I'm not sure how this would work. digitalCrownRotation isn't like a DragGesture that has an onEnded callback. Therefore, you can leave Pager stuck in between 2 pages. This works great with a ScrollView but Pager isn't just a ScrollView. See video below for more info:

https://user-images.githubusercontent.com/11335612/161589983-6950e704-c5be-400d-8059-f70b4beb8ec8.mp4

andriy-appuchino commented 2 years ago

@fermoya Look at my example, the important part is to set from: 0, through: 10, by: 1. Where the through value is the number of pages. When rotating the crown and the value is fractional, you need to move the pages in the same way as you did with the drag gesture. But when you stop rotating the crown, the value will automatically go to a non-fractional value, which will work like onEded, and will already be stopped on a specific page.

andriy-appuchino commented 2 years ago

@fermoya could you try please?

fermoya commented 2 years ago

Oh I see now what you mean. I'll come back to you shortly

fermoya commented 2 years ago

@andriy-appuchino check out 2.4.0-beta.1. Notice that this works with watchOS 7.0 onwards

andriy-appuchino commented 2 years ago

@fermoya Thank you for your efforts. I tried it, but it works about the same as my example. I'll try to clarify. Now when the crown rotates, the content does not move, but only after stopping, with some delay. It's not like the digital crown works on a watch. My expectation is that when rotating the crown, the content will also move, as if you are using a drag gesture, and then it centers the page. Could you improve this?

Here I made an example to demonstrate the logic I mean:

https://user-images.githubusercontent.com/49787488/161800728-a06876c3-4517-47bb-adee-1cae2cd83430.mp4

    var body: some View {
        GeometryReader { proxy in
            List {
                Group {
                    Color.blue
                    Color.green
                    Color.yellow
                    Color.orange
                    Color.red
                }
                .listRowBackground(Color.clear)
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                .frame(height: proxy.size.height)
            }
            .listStyle(.elliptical)
        }
    }

You can try this code for yourself. If you do not have an Apple Watch, then on the watch simulator a digital crown rotating is triggered by moving two fingers.

fermoya commented 2 years ago

@andriy-appuchino I've been testing in a simulator, didn’t you see my first video? I haven’t merely copy-pasted your example if that’s what you suggest 😅

digitalCrownRotation doesn’t work the same as DragGesture as you imply. DragGesture has an onEnded callback (or alternatively, a GestureState that resets). digitalCrownRotation doesn’t, it just updated a Binding. There’s no way to know when the rotation finished. So to ensure the page is centered I can just update the page index.

Any suggestions are more than welcome but this is the best it can be done as far as I can see.

Feel free to fork the repo, peek the code (PagerContent.body), make changes and send a PR.

andriy-appuchino commented 2 years ago

Well, yes, that's right. When the value changes to a fractional, then you need to move the content as you do with a drag gesture, and when the value becomes a non-fractional number, then just consider it fired onEnded.

fermoya commented 2 years ago

print the value observed in onChanged, it's always a fractional! What you suggest is already done. Whenever the rotation goes over 0.5, page index is incremented/decremented.

All in all, it can't be done in a linear fashion so to speak, unfortunately. An onEnded callback or any other workaround would be needed

andriy-appuchino commented 2 years ago

OK, thank you. I already made a fork, I'll try to figure it out. I'll let you know if it works.

fermoya commented 2 years ago

Thanks @andriy-appuchino , all help is much appreciated. Keep me posted, I'd like to make a release within the next days. If you find some possible improvement I'll include it

andriy-appuchino commented 2 years ago

@fermoya I managed to do it, I can't wait for you to try https://github.com/fermoya/SwiftUIPager/pull/268

fermoya commented 2 years ago

new 2.4.0-beta.2 pushed

andriy-appuchino commented 2 years ago

Great, it works as it should! 🎉 Thank you.

andriy-appuchino commented 2 years ago

@fermoya by the way, when will the release be?

fermoya commented 2 years ago

I'll make a release within 2-3 days tops most. I want to see if I can resolve an issue with a pipeline, first.

Anyway, I'll make a comment here when the new version is out

andriy-appuchino commented 2 years ago

@fermoya I caught a bug.

Initial:

  1. Two pages
  2. We are on page 1

Steps:

  1. Swipe to page 2
  2. Rotate the crown in the same direction as if you wanted to go to page 3 (which doesn't exist)

Expectations: Nothing should happen.

Result: The page bounces, the position indicator on the right shows movement from page 1 to page 2.

andriy-appuchino commented 2 years ago

After switching the page with a swipe, you need to change the state of the digital crown digitalCrownPageOffset, you apparently missed it. It was in my PR, you can see how I did it.

fermoya commented 2 years ago

Should be solved in beta-4

andriy-appuchino commented 2 years ago

There was another problem on watchOS. If Pager is in TabView like in an example, then switching between tabs from left to right very often does not work. Probably Pager is blocking gestures horizontally somewhere.

struct ContentView: View {
    enum Tab {
        case controls, main, player
    }

    @State private var selection: Tab = .main

    @StateObject var page: Page = .first()
    var items = Array(0..<2)

    var body: some View {
        TabView(selection: $selection) {
            ControlsView()
                .tag(Tab.controls)

            Pager(page: page,
                  data: items,
                  id: \.self,
                  content: { index in
                Text("Page: \(index)")
            })
            .vertical()
            .tag(Tab.main)

            NowPlayingView()
                .tag(Tab.player)
        }
        .tabViewStyle(PageTabViewStyle())
    }
}
fermoya commented 2 years ago

@andriy-appuchino please open a new ticket, I think it's unrelated with this thread.

seems to be working for me anyways:

https://user-images.githubusercontent.com/11335612/162452955-9c09a63d-d847-4a02-868b-87206095ed85.mp4

andriy-appuchino commented 2 years ago

@fermoya When there is only Text on the page, the problem is less noticeable because the swipe is blocked only in the text area. I created an issue https://github.com/fermoya/SwiftUIPager/issues/274 and attached a project where the pages are filled with color. There is a very high reproducibility.