Git Product home page Git Product logo

scrollviewproxy's Introduction

As of June 22 2020 this is included in the SwiftUI 2 beta. https://developer.apple.com/documentation/swiftui/scrollviewproxy

The Apple implementation uses just .id(_:) and I had update issues with that where Views with an id sometimes won't update when their ObservedObject changed. Maybe this has been fixed in the new SwiftUI 2 beta.

Also the Apple implementation only supports iOS 14 so I think this repo is still useful for backwards compatibility.

Note: An important difference between this library and Apples implementation is that the ScrollViewReader goes inside the ScrollView. If you place the ScrollViewReader around the ScrollView the scrollTo function will not scroll to the correct location (due to its coordinateSpace not being part of the scrolling content). I considered fixing this to align with Apple but that would break backwards compatibility with projects already using ScrollViewProxy.

Maybe it's possible to detect if we're inside or outside of a ScrollView in the reader but then how do we handle nested ScrollViews? If you have any ideas please open an Issue/PR or email me.

ScrollViewProxy

Adds ScrollViewReader and ScrollViewProxy that help you scroll to locations in a ScrollView

To get a ScrollViewProxy you can either use the conveinience init on ScrollView

ScrollView { proxy in
    ...
}

or add a ScrollViewReader to any View that creates a UIScrollView under the hood

List {
    ScrollViewReader { proxy in
        ...
    }
}

The ScrollViewProxy currently has one variable and two functions you can call

/// A publisher that publishes changes to the scroll views offset
public var offset: OffsetPublisher

/// Scrolls to an edge or corner
public func scrollTo(_ alignment: Alignment, animated: Bool = true)

/// Scrolls the view with ID to an edge or corner
public func scrollTo(_ id: ID, alignment: Alignment = .top, animated: Bool = true)

To use the scroll to ID function you have to add an ID to the view you want to scroll to

ScrollView { proxy in
    HStack { ... }
        .scrollId("someId")
}

This is the only part that is different from the SwiftUI 2.0 implementation because I don't know how to access Views by ID from the .id(_:) function

Example

Everything put together in an example

struct ScrollViewProxyExample: View {
    
    @State var randomInt = Int.random(in: 0..<200)
    @State var proxy: ScrollViewProxy? = nil
    @State var offset: CGPoint = .zero
    
    var body: some View {
        // GeometryReader for safeAreaInsets on Sticky View
        GeometryReader { geometry in 
            VStack {
                ScrollView { proxy in
                    Text("Sticky View")
                        .background(Color.white)
                        .onReceive(proxy.offset) { self.offset = $0 }
                        .offset(x: offset.x, y: offset.y + geometry.safeAreaInsets.top)
                        .zIndex(1)
                    
                    VStack(spacing: 20) {
                        ForEach(0..<200) { index in
                            HStack {
                                Spacer()
                                Text("Item: \(index)").font(.title)
                                Spacer()
                            }.scrollId(index)
                        }
                    }
                    .zIndex(0)
                    .onAppear {
                        self.proxy = proxy
                    }
                }
                HStack {
                    Button(action: {
                        self.proxy?.scrollTo(self.randomInt, alignment: .center)
                        self.randomInt = Int.random(in: 0..<200)
                    }, label: {
                        Text("Go to \(self.randomInt)")
                    })
                    Spacer()
                    Button(action: { self.proxy?.scrollTo(.bottom) }, label: {
                        Text("Bottom")
                    })
                    Spacer()
                    Button(action: { self.proxy?.scrollTo(.center) }, label: {
                        Text("Center")
                    })
                }.padding()
            }
        }
    }
}

Deintegrate

Want to drop iOS 13 support and move to the SwiftUI 2.0 version?

  1. Remove the Package
  2. Add this extension:
extension View {
    public func scrollId<ID: Hashable>(_ id: ID) -> some View {
        id(id)
    } 
}

(Or replace all .scrollId with .id)

scrollviewproxy's People

Contributors

amzd avatar andrewsb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

scrollviewproxy's Issues

Performance of change state on change scroll

Hello! I have a performance issue.

Let's say we have this code to hide top view when user scrolling a bit:

import SwiftUI
import struct ScrollViewProxy.ScrollViewProxy

struct MenuView: View {
    
    @State private var proxy: ScrollViewProxy?
    
    @State private var topOpacity: Double = 1.0
    @State private var topHeight: CGFloat? = nil

    var body: some View {
        ZStack {
            Color.white.edgesIgnoringSafeArea(.all)
            
            VStack {
                HStack {
                    Button(action: {}) {
                        Image(systemName: "chevron.left")
                            .font(.system(size: 18, weight: .medium))
                            .foregroundColor(.blueDark)
                            .padding(.horizontal, 16.0)
                    }
                    
                    Text("A Header")
                        .lineLimit(1)
                        .font(.system(size: 26, weight: .bold))
                        .foregroundColor(.blueDark)
                    
                    Spacer()
                }
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .frame(height: topHeight)
                .opacity(topOpacity)
                .clipped()
          
                ScrollView(.vertical, showsIndicators: false) { proxy in
                    VStack(alignment: .leading) {
                        ForEach(categories) { category in
                            Text(category.name)
                                .font(.system(size: 40, weight: .medium))
                                .foregroundColor(.blueDark)
                                .padding(.top, 30.0)
                                .padding(.bottom, 18.0)
                                .scrollId(category.id)
                                .onReceive(proxy.offset) {
                                    let height = 50 - $0.y
                                    self.topHeight = height < 0 ? 0 : height
                                    let opacity: Double = Double(20 - $0.y) / 20
                                    self.topOpacity = opacity < 0 ? 0 : opacity
                                }
                        }
                    }
                    .padding(.bottom, 16.0)
                    .onAppear {
                        self.proxy = proxy
                    }
                }
                .background(Color.light.edgesIgnoringSafeArea(.all))
                .overlay(Divider().background(Color.grayLight), alignment: .top)
                .overlay(Divider().background(Color.grayLight), alignment: .bottom)
                .padding(.bottom, 1)
            }
            .animation(.easeIn)
            .edgesIgnoringSafeArea(orderShow ? .horizontal : .bottom)
        }
    }
    
}

But the performance of this action is incredibly slow, it's freezing and "skipping frames" when updating State variables, maybe I'm missing something?

Does not work with macOS

The Package.swift states that the package should work with macOS (.macOS(.v10_13),) but since UIScrollView does not exist on macOS (it's called NSScrollView there instead) it doesn't.

Introspect seems to provide NSScrollView on macOS so maybe its just a quick fix but I don't know how much NS and UI APIs differ. With a bit of luck its only this declaration that breaks it

What would you do for a `LazyVStack` ?

Hi!
Amazing helper for iOS 13 support! thank you, appreciated here!
I was wondering, I have performance issues since i have a lot of data. I wanted to use a LazyVStack but I am starting to have issues like this:

ID (sb-l2-000021) not found, make sure to add views with `.id(_:scrollView:)`. Did find: [AnyHashable("sb-l2-000004"): (0.0, 682.4000000000001, 360.3333333333333, 168.0), AnyHashable("sb-l2-000005"): (0.0, 870.4000000000001, 370.66666666666663, 196.0), AnyHashable("sb-l2-000008"): (0.0, 1322.4, 371.0, 140.0), AnyHashable("sb-l2-000003"): (0.0, 550.4000000000001, 357.66666666666663, 112.0), AnyHashable("sb-l2-000001"): (0.0, 314.4000000000001, 358.66666666666663, 112.0), AnyHashable("sb-l2-000006"): (0.0, 1086.4, 347.66666666666663, 140.0), AnyHashable("sb-l2-000009"): (0.0, 1482.4, 365.0, 84.0), AnyHashable("sb-l2-000013"): (0.0, 2010.4, 374.0, 140.0), AnyHashable("sb-l2-000011"): (0.0, 1830.4, 310.66666666666663, 56.0), AnyHashable("sb-l2-000012"): (0.0, 1906.4, 374.0, 84.0), AnyHashable("sb-l2-000002"): (0.0, 446.4000000000001, 374.0, 84.0), AnyHashable("sb-l2-000007"): (0.0, 1246.4, 374.0, 56.0), AnyHashable("sb-l2-000010"): (0.0, 1586.4, 374.0, 224.0)]

I believe the fact that I am using a LazyVStack, it hasn't load the Text() with their respective ids. What do you guys recommend to do here?
Thanks for any future help!

Scrolling does not work in .onAppear { }

Hello!

Thanks for ScrollViewProxy, great work πŸ‘

I need to implement messages UI (so inverse scroll view, starting at the bottom). However using ScrollViewProxy in onAppear doesn't seem to be working correctly.

struct ContentView: View {
  var body: some View {
    ScrollView { proxy in
      VStack(spacing: 20) {
        ForEach(0..<20) { index in
          Rectangle()
            .stroke(Color.blue)
            .frame(width: 150, height: 50)
            .overlay(Text("\(index)"))
        }
      }
      .onAppear {
        proxy.scrollTo(.bottom, animated: false)
      }
    }
  }
}

The same does work correctly with Apple's ScrollViewReader, however that's iOS 14 only. From what I've investigated it's because the scrollView is not set on the coordinator at the time of the call, so it's ignored here:

guard let scrollView = coordinator.scrollView else { return }

There's a workaround to use DispatchQueue.main.async { proxy.scrollTo(.bottom, animated: false) }, but that's ugly and doesn't cooperate correctly with snapshot testing 😞

MIT licence

@Amzd, @AndrewSB
Hi guys. In my application, for all the libraries used, I need to provide a link to the mit license. Could you add mit license to your repository? Thanks in advance

Performance / 100% CPU issue

Hi

Thanks for providing this proxy so it's possible to scroll to specific items on iOS13 as well.

I noticed a problem when I added it to my app - as soon as I included it in a view and did the
ScrollView { proxy in .... the CPU usage went to 100% while just having the view loaded on screen. (without using the proxy for anyting)

Since all the code is in one file, I copied it in (instead of doing a fork) and fixed the issue with the following if sentence in the .introspectScrollView.

I haven't found anything not working after that if has been addedπŸ˜…

                .introspectScrollView {
                    if self.proxy.coordinator.scrollView != $0 {
                        self.proxy.coordinator.scrollView = $0
                        self.proxy.offset = $0.offsetPublisher
                    }
                }

It still feels a bit scary that the introspectScrollView is being executed constantly, but at least this if-guard ensures that the rest of the app-state doesn't have to update.

Nested ScrollView stops scrollTo function

iOS 13, When we enabled the horizontal scrollview(Test View) inside the main scrollview, scrollTo function doesn't working.
Looking some solution to put horizontal scrollview inside the vertical scrollview.
Thank you.

var body: some View {
    VStack(alignment: .leading) {
        //MARK:- Top horizontal category list
        HStack(spacing: 30) {
            ForEach(categories, id: \.self) { category in
                Button(action: {
                    if let proxy = proxy {
                        proxy.scrollTo(category, alignment: .top, animated: true)
                    }
                }) {
                    Text(category)
                        .fixedSize(horizontal: true, vertical: true)
                }
            }
        }
        .padding(.horizontal)
        
        //MARK:- Main vertical ScrollView
        ScrollView(.vertical, showsIndicators: false) { proxy in
            VStack(alignment: .leading, spacing: 30) {
                
                //MARK:- Horizontal ScrollView
                ScrollView(.horizontal, showsIndicators: false) {
                //When we enabled the horizontal scrollview here, scroll to function doesn't working.
                    HStack{
                        Text("Test view")
                            .font(.title)
                            .bold()
                    }
                }
                
                //MARK:- Main " Subviews
                ForEach(categories, id: \.self) { category in
                    VStack(alignment: .leading) {
                        Text(category)
                            .font(.largeTitle)
                            .foregroundColor(Color.black)
                            .onTapGesture {
                                proxy.scrollTo(category)
                            }
                        VStack(alignment: .leading) {
                            ForEach(0..<5) { index in
                                Text("Subview \(index)")
                                    .padding()
                            }
                        }.padding(.top)
                    }
                    .scrollId(category)
                }
            }
            .onAppear { self.proxy = proxy }
        }
    }
    .padding(.horizontal)
}

Not scrolling to the right position!

When enabled text modifiers on the button body, scrollTo function not working properly. It's scrolling random positions. Looking for a solution. Thanks.

Screen.Recording.2021-05-14.at.1.19.34.AM.mp4

Remove ScrollViewProxy.ID

Remove ScrollViewProxy.ID and just cast to AnyHashable. I think the Apple implementation does this too. This will make it easier for people to migrate once they drop iOS 13 support.

coordinator.frames = []

I'm debugging an issue where the coordinator for the proxy has no frames, so I can't scroll to any ID.

Usage:

List {
            VStack(alignment: .leading, spacing:0) {
                Group {
                        ....
                }
            }


            ScrollViewReader {
                 ForEach(..) {
                        someView.id(someUUID)
                   }
             }
}

Horizontal Scroll

Hey bro, thanks for this extension but I want to use it for horizontal scrollView but I got an error. Is this not possible for use as horizontal ?

FYI, `SwiftUI-Introspect` 0.2.0 changed min iOS to 13, breaking builds

FYI, wanted to let you know that projects including ScrollViewProxy will likely break now that https://github.com/siteline/SwiftUI-Introspect/releases/tag/0.2.0 has a minimum iOS 13, but this project has min iOS 11. Can either try and upgrade this project to use the latest introspect library, or update the Package.swift to only include the exact version "0.1.4".

Workaround:
If someone includes this package as a dependency, you must also include the correct SwiftUI-Introspect version before the iOS minimum changed, like this:

    dependencies: [
        ...
        .package(url: "https://github.com/Amzd/ScrollViewProxy", exact: "1.0.3"),
        .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", exact: "0.1.4"),
        ...
    ]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.