Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A scrollable SwiftUI view, UIScrollView wrapper. ScrollableView lets you read and write content offsets for scrollview in SwiftUI, with and without animations.
import SwiftUI
struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {
// MARK: - Coordinator
final class Coordinator: NSObject, UIScrollViewDelegate {
// MARK: - Properties
private let scrollView: UIScrollView
var offset: Binding<CGPoint>
// MARK: - Init
init(_ scrollView: UIScrollView, offset: Binding<CGPoint>) {
self.scrollView = scrollView
self.offset = offset
super.init()
self.scrollView.delegate = self
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
self.offset.wrappedValue = scrollView.contentOffset
}
}
}
// MARK: - Type
typealias UIViewControllerType = UIScrollViewController<Content>
// MARK: - Properties
var offset: Binding<CGPoint>
var animationDuration: TimeInterval
var showsScrollIndicator: Bool
var axis: Axis
var content: () -> Content
var onScale: ((CGFloat)->Void)?
var disableScroll: Bool
var forceRefresh: Bool
var stopScrolling: Binding<Bool>
private let scrollViewController: UIViewControllerType
// MARK: - Init
init(_ offset: Binding<CGPoint>, animationDuration: TimeInterval, showsScrollIndicator: Bool = true, axis: Axis = .vertical, onScale: ((CGFloat)->Void)? = nil, disableScroll: Bool = false, forceRefresh: Bool = false, stopScrolling: Binding<Bool> = .constant(false), @ViewBuilder content: @escaping () -> Content) {
self.offset = offset
self.onScale = onScale
self.animationDuration = animationDuration
self.content = content
self.showsScrollIndicator = showsScrollIndicator
self.axis = axis
self.disableScroll = disableScroll
self.forceRefresh = forceRefresh
self.stopScrolling = stopScrolling
self.scrollViewController = UIScrollViewController(rootView: self.content(), offset: self.offset, axis: self.axis, onScale: self.onScale)
}
// MARK: - Updates
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIViewControllerType {
self.scrollViewController
}
func updateUIViewController(_ viewController: UIViewControllerType, context: UIViewControllerRepresentableContext<Self>) {
viewController.scrollView.showsVerticalScrollIndicator = self.showsScrollIndicator
viewController.scrollView.showsHorizontalScrollIndicator = self.showsScrollIndicator
viewController.updateContent(self.content)
let duration: TimeInterval = self.duration(viewController)
let newValue: CGPoint = self.offset.wrappedValue
viewController.scrollView.isScrollEnabled = !self.disableScroll
if self.stopScrolling.wrappedValue {
viewController.scrollView.setContentOffset(viewController.scrollView.contentOffset, animated:false)
return
}
guard duration != .zero else {
viewController.scrollView.contentOffset = newValue
return
}
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveEaseInOut, .beginFromCurrentState], animations: {
viewController.scrollView.contentOffset = newValue
}, completion: nil)
}
func makeCoordinator() -> Coordinator {
Coordinator(self.scrollViewController.scrollView, offset: self.offset)
}
//Calcaulte max offset
private func newContentOffset(_ viewController: UIViewControllerType, newValue: CGPoint) -> CGPoint {
let maxOffsetViewFrame: CGRect = viewController.view.frame
let maxOffsetFrame: CGRect = viewController.hostingController.view.frame
let maxOffsetX: CGFloat = maxOffsetFrame.maxX - maxOffsetViewFrame.maxX
let maxOffsetY: CGFloat = maxOffsetFrame.maxY - maxOffsetViewFrame.maxY
return CGPoint(x: min(newValue.x, maxOffsetX), y: min(newValue.y, maxOffsetY))
}
//Calculate animation speed
private func duration(_ viewController: UIViewControllerType) -> TimeInterval {
var diff: CGFloat = 0
switch axis {
case .horizontal:
diff = abs(viewController.scrollView.contentOffset.x - self.offset.wrappedValue.x)
default:
diff = abs(viewController.scrollView.contentOffset.y - self.offset.wrappedValue.y)
}
if diff == 0 {
return .zero
}
let percentageMoved = diff / UIScreen.main.bounds.height
return self.animationDuration * min(max(TimeInterval(percentageMoved), 0.25), 1)
}
// MARK: - Equatable
static func == (lhs: ScrollableView, rhs: ScrollableView) -> Bool {
return !lhs.forceRefresh && lhs.forceRefresh == rhs.forceRefresh
}
}
final class UIScrollViewController<Content: View> : UIViewController, ObservableObject {
// MARK: - Properties
var offset: Binding<CGPoint>
var onScale: ((CGFloat)->Void)?
let hostingController: UIHostingController<Content>
private let axis: Axis
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.canCancelContentTouches = true
scrollView.delaysContentTouches = true
scrollView.scrollsToTop = false
scrollView.backgroundColor = .clear
if self.onScale != nil {
scrollView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(self.onGesture)))
}
return scrollView
}()
@objc func onGesture(gesture: UIPinchGestureRecognizer) {
self.onScale?(gesture.scale)
}
// MARK: - Init
init(rootView: Content, offset: Binding<CGPoint>, axis: Axis, onScale: ((CGFloat)->Void)?) {
self.offset = offset
self.hostingController = UIHostingController<Content>(rootView: rootView)
self.hostingController.view.backgroundColor = .clear
self.axis = axis
self.onScale = onScale
super.init(nibName: nil, bundle: nil)
}
// MARK: - Update
func updateContent(_ content: () -> Content) {
self.hostingController.rootView = content()
self.scrollView.addSubview(self.hostingController.view)
var contentSize: CGSize = self.hostingController.view.intrinsicContentSize
switch axis {
case .vertical:
contentSize.width = self.scrollView.frame.width
case .horizontal:
contentSize.height = self.scrollView.frame.height
}
self.hostingController.view.frame.size = contentSize
self.scrollView.contentSize = contentSize
self.view.updateConstraintsIfNeeded()
self.view.layoutIfNeeded()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.createConstraints()
self.view.setNeedsUpdateConstraints()
self.view.updateConstraintsIfNeeded()
self.view.layoutIfNeeded()
}
// MARK: - Constraints
fileprivate func createConstraints() {
NSLayoutConstraint.activate([
self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
}
@stash-196

This comment has been minimized.

Copy link

@stash-196 stash-196 commented Jan 20, 2020

Hi,
I saw your answer on stack-overflow, and was interested. If I may ask, can you give some pointers on how your code is supposed to be handled??

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Jan 20, 2020

A usage would look some like:

struct MyView: View {

	@State private var contentOffset: CGPoint = .zero

	var body: some View {
		
		ScrollableView(self.$contentOffset, animationDuration: 0.5) {
			VStack {
				Button(action: {
					self.contentOffset = CGPoint(x: 0, y: 0)
				}) {
					Text("scroll to top")
				}
				
				Text("example text 0")
				Text("example text 1")
				Text("example text 2")
				Text("example text 3")
				Text("example text 4")
				
				Button(action: {
					self.contentOffset = CGPoint(x: 0, y: 100)
				}) {
					Text("scroll to bottom")
				}
			}
		}
	}
}
@stash-196

This comment has been minimized.

Copy link

@stash-196 stash-196 commented Jan 23, 2020

Thanks! I always frustrated not being able to change the offset for a horizontal ScrollView. This is a life saver!

@sjang42

This comment has been minimized.

@tsabend

This comment has been minimized.

Copy link

@tsabend tsabend commented Feb 14, 2020

It seems like there is an issue with the initial rendering of the scrollView because the constraints aren't taking effect soon enough which leads the non-scrolling axis to be set too small. Either explicitly setting the scrollView's initial frame or calling layoutIfNeeded() on it in viewDidLoad resolves this.

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Feb 18, 2020

It seems like there is an issue with the initial rendering of the scrollView because the constraints aren't taking effect soon enough which leads the non-scrolling axis to be set too small. Either explicitly setting the scrollView's initial frame or calling layoutIfNeeded() on it in viewDidLoad resolves this.

Thanks, I've updated the gist to include a call to layoutIfNeeded() in viewDidLoad()

@arbnori45

This comment has been minimized.

Copy link

@arbnori45 arbnori45 commented Feb 24, 2020

NavigationLink is not working when clicking on the cell of a view.

@ablyes

This comment has been minimized.

Copy link

@ablyes ablyes commented Mar 17, 2020

Nice home made scrollableView.
I'm using it it, it saved my life.
... but i still have an issue with it : i have first to click on the scrollbar to let it appear.
If I comment the call : self.view.layoutIfNeeded() -> than i have the scrollableView displayed in a wrong place (because of not applied constraints ?).
Could you please help me ?

@ablyes

This comment has been minimized.

Copy link

@ablyes ablyes commented Mar 18, 2020

it's better with this code :

   override func viewDidLoad() {
        print("viewDidLoad")
        self.view.addSubview(self.scrollView)
        self.createConstraints()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        print("viewWillAppear")
        self.view.layoutIfNeeded()
    }

The scrollableView "Serie" is used into main view, and detail view.
In the mainView the display is correct, in the detail view ... i have to click somewhere on the screen to get the scrollableView in right place.
Very strange.

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Mar 18, 2020

Thanks for the feedback @ablyes, I've updated the gist with updated view will appear / view did load calls. I failed to call super in view did load, so hopefully that fixes a few of the issues you are seeing

@ablyes

This comment has been minimized.

Copy link

@ablyes ablyes commented Mar 18, 2020

Thank you for the update.
Even with you changes, i still face the same problem.
With your last changes :
When I call self.view.layoutIfNeeded() in viewDidLoad :
Can't see the scrollableView on the detail screen until I touch the scollable zone, or click a button.

When comment the call to self.view.layoutIfNeeded() in viewDidLoad :
I have to click somewhere on the screen to get the scrollableView in right place (otherwise it's placed 30 px higher than expected).

For info, i'm using it with axe = .horizontal.

@ablyes

This comment has been minimized.

Copy link

@ablyes ablyes commented Mar 18, 2020

I'm spent a lot of time without any success.
I even used Singleton to get the same instance of my ScrollableView on Main and Detail screens.
Of course... the problem is related to the rendering, and it doesn't change anything.

Hope I can hear something from you soon.

@ablyes

This comment has been minimized.

Copy link

@ablyes ablyes commented Mar 19, 2020

May be the reason why is just because something goes wrong if the view is not used initially in the SceneDelegate ?
What is done on the SceneDelegate that I should do to get this the ScrollableView working in Detail Screen ?

@suniique

This comment has been minimized.

Copy link

@suniique suniique commented Mar 23, 2020

Thank you for the update.
Even with you changes, i still face the same problem.
With your last changes :
When I call self.view.layoutIfNeeded() in viewDidLoad :
Can't see the scrollableView on the detail screen until I touch the scollable zone, or click a button.

When comment the call to self.view.layoutIfNeeded() in viewDidLoad :
I have to click somewhere on the screen to get the scrollableView in right place (otherwise it's placed 30 px higher than expected).

For info, i'm using it with axe = .horizontal.

I met with the same problem. I really don't know why but I found a simple workaround that setting a small perturbation to the offset in viewDidLoad(). Just a temporary solution.

override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.scrollView)
        self.createConstraints()
        self.view.layoutIfNeeded()
        self.offset.wrappedValue = CGPoint(x: 1e-10, y: 0.0)  // cannot be CGPoint(x: 0, y: 0)
    }
@famictech2000

This comment has been minimized.

Copy link

@famictech2000 famictech2000 commented Apr 15, 2020

This does NOT work!?

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Apr 29, 2020

I've updated the gist with what I have as a working version in a live app. I have not fully explored by horizontal scrolling works, but vertical scrolling has been consistently working. Please give this a go, hope it works for everyone!

@guidedways

This comment has been minimized.

Copy link

@guidedways guidedways commented Apr 30, 2020

Just tested this with a horizontal list - does not work at all. Strangely it offset the contents with some initial offset and the scrollable list appears to be frozen.

@guidedways

This comment has been minimized.

Copy link

@guidedways guidedways commented Apr 30, 2020

Okay it seems it works for a horizontal list as long as I manually try and scroll the contents, otherwise they appear as invisible on an initial launch. Moreover I see Modifying state during view update, this will cause undefined behavior. reported for scrollViewDidScroll on an initial scroll.

@CypherPoet

This comment has been minimized.

Copy link

@CypherPoet CypherPoet commented May 21, 2020

@guidedways The scrollViewDidScroll issue seems like something a handy DispatchQueue.main.async will fix.

@mlm249

This comment has been minimized.

Copy link

@mlm249 mlm249 commented May 26, 2020

This is great and seems to be working for the most part for my use case (vertical scroll view).

I do wanna echo the others in saying that something is blocking the UI after I scroll via setting contentOffset. Once I manually scroll after that the UI functions normally. I'm wondering if this could be refactored to use the Coordinator pattern to handle updates back to the swiftui side.

Anyway thanks for the gist!

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented May 27, 2020

Thanks for all of the feedback, I'm glad to hear this is generally working for people. I've added a Coordinator object and Dispatch Queue Main sync on offset update.

I've also updated setNeedsLayout / layoutIfNeed to the constraint variation to setNeedsUpdateConstraints / updateConstraintsIfNeeded cover more potential cases. The latter will call the former afterwards, so this should have no negative effect in regards to layout.

@jakorten

This comment has been minimized.

Copy link

@jakorten jakorten commented May 31, 2020

If I use the GIST I don't see anything, if I add self.view.updateConstraintsIfNeeded() it shows and works fine.

B.t.w. I would like to suggest to reverse buttons of the example from @jfuellert on 20 Jan 2020 and also change CGPoint(x: 0, y: 500) to CGPoint(x: 0, y: 100) instead as it otherwise scrolls 'out of view'.

@timtraver

This comment has been minimized.

Copy link

@timtraver timtraver commented May 31, 2020

This is a great code snippet, and I really appreciate that you have continued to update it with suggestions.

I was having a problem with the initial view of the scrollview content being shifted in the horizontal direction by 50% in a vertical scrollview area. As soon as I touch the area to scroll it, it snaps to be correct.

On chance I decide to try to add self.view.layoutIfNeeded() next to your self.view.updateConstraintsIfNeeded() and it fixed the problem.

So you may want to add that line in the viewDidLoad and the updateContent areas.

Otherwise it works great!

Edit : you don't need it in the updateContent area, or it will continually update the scrollable view and you can see it get jittery

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Jun 1, 2020

Updated the Gist by adding self.view.layoutIfNeeded() to updateContent() and updating the usage example. Thanks everyone!

@timtraver

This comment has been minimized.

Copy link

@timtraver timtraver commented Jun 1, 2020

Actually, the initial screen being offeset horizontally was fixed for me by putting self.view.layoutIfNeeded() in the viewDidLoad. If you only put it in the updateContent, then it only happens after you touch the scrollable area...

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Jun 1, 2020

Updated!

@mlm249

This comment has been minimized.

Copy link

@mlm249 mlm249 commented Jun 1, 2020

This fixed the update issue for me, thanks for the speedy response times

@pboulch

This comment has been minimized.

Copy link

@pboulch pboulch commented Jun 30, 2020

Hi !
I would like to have a listener on the ScrollableView to detect when the user scrolls and to know the value of the offset. I was thinking of using the preferences and calling it from the scrollViewDidScroll but I'm not sure if this is possible. Do you have an opinion on the matter? Thank you in advance !

@halilyuce

This comment has been minimized.

Copy link

@halilyuce halilyuce commented Jul 5, 2020

Thanks @jfuellert but I've got a problem about and I read all comments, I'm not only a person who facing this issue. I'm trying to use a horizontal scroll but it does not seem before touch on it. Interesting bug, is there any solution for that ?

@jfuellert

This comment has been minimized.

Copy link
Owner Author

@jfuellert jfuellert commented Jul 6, 2020

No solution hard for that one yet, though it depends on your implementation of the scroll view itself.
I'm using horizontal scroll in two places, but both of which I'm forcing a width (infinity) and pre-setting a scroll offset to zero.

@Mr-Perfection

This comment has been minimized.

Copy link

@Mr-Perfection Mr-Perfection commented Jul 10, 2020

Meh, I guess most ppl got this working... I'm getting this error

extensions of generic classes cannot contain '@objc' members

----------------------------------------

CompileDylibError: Failed to build ScrollableView.swift

Compiling failed: extensions of generic classes cannot contain '@objc' members

/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:50:59: error: extensions of generic classes cannot contain '@objc' members
    @_dynamicReplacement(for: viewDidLoad()) private func __preview__viewDidLoad() {
                                                          ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:50:6: error: 'viewDidLoad()' is marked @objc dynamic
    @_dynamicReplacement(for: viewDidLoad()) private func __preview__viewDidLoad() {
     ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:87:65: error: extensions of generic classes cannot contain '@objc' members
    @_dynamicReplacement(for: onGesture(gesture:)) private func __preview__onGesture(gesture: UIPinchGestureRecognizer) {
                                                                ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:87:6: error: 'onGesture(gesture:)' is marked @objc dynamic
    @_dynamicReplacement(for: onGesture(gesture:)) private func __preview__onGesture(gesture: UIPinchGestureRecognizer) {
     ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:178:40: error: ambiguous type name 'Coordinator' in 'ScrollableView'
typealias Coordinator = ScrollableView.Coordinator
                        ~~~~~~~~~~~~~~ ^
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:14:17: note: found candidate with type 'ScrollableView.Coordinator'
    final class Coordinator: NSObject, UIScrollViewDelegate {
                ^
SwiftUI.UIViewControllerRepresentable:9:20: note: found candidate with type 'Self.Coordinator'
    associatedtype Coordinator = Void
                   ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:179:49: error: ambiguous type name 'UIViewControllerType' in 'ScrollableView'
typealias UIViewControllerType = ScrollableView.UIViewControllerType
                                 ~~~~~~~~~~~~~~ ^
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:37:15: note: found candidate with type 'ScrollableView<Content>.UIViewControllerType'
    typealias UIViewControllerType = UIScrollViewController<Content>
              ^
SwiftUI.UIViewControllerRepresentable:5:20: note: found candidate with type 'Self.UIViewControllerType'
    associatedtype UIViewControllerType : UIViewController
                   ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:180:22: error: reference to generic type 'ScrollableView' requires arguments in <...>
private func ==(lhs: ScrollableView, rhs: ScrollableView) -> Bool {
                     ^
                                   <<#Content: View#>>
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:11:8: note: generic type 'ScrollableView' declared here
struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {
       ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:180:43: error: reference to generic type 'ScrollableView' requires arguments in <...>
private func ==(lhs: ScrollableView, rhs: ScrollableView) -> Bool {
                                          ^
                                                        <<#Content: View#>>
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:11:8: note: generic type 'ScrollableView' declared here
struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {
       ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:180:14: error: operator '==' declared in extension of 'ScrollableView.Coordinator' must be 'static'
private func ==(lhs: ScrollableView, rhs: ScrollableView) -> Bool {
             ^
static 
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:183:29: error: reference to generic type 'ScrollableView' requires arguments in <...>
private static func ==(lhs: ScrollableView, rhs: ScrollableView) -> Bool {
                            ^
                                          <<#Content: View#>>
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:11:8: note: generic type 'ScrollableView' declared here
struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {
       ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:183:50: error: reference to generic type 'ScrollableView' requires arguments in <...>
private static func ==(lhs: ScrollableView, rhs: ScrollableView) -> Bool {
                                                 ^
                                                               <<#Content: View#>>
/Users/stephenlee/Developer/techat/tennis chat app/Views/Components/ScrollableView.swift:11:8: note: generic type 'ScrollableView' declared here
struct ScrollableView<Content: View>: UIViewControllerRepresentable, Equatable {
       ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:187:69: error: extensions of classes from generic context cannot contain '@objc' members
    @_dynamicReplacement(for: scrollViewDidScroll(_:)) private func __preview__scrollViewDidScroll(_ scrollView: UIScrollView) {
                                                                    ^
/Users/stephenlee/Library/Developer/Xcode/DerivedData/tennis_chat_app-fbonerrlqshkvwgumvsmbispqsgp/Build/Intermediates.noindex/Previews/DEV tennis chat app/Intermediates.noindex/tennis chat app.build/Debug-iphonesimulator/tennis chat app dev.build/Objects-normal/x86_64/ScrollableView.2.preview-thunk.swift:187:6: error: 'scrollViewDidScroll' is marked @objc dynamic
    @_dynamicReplacement(for: scrollViewDidScroll(_:)) private func __preview__scrollViewDidScroll(_ scrollView: UIScrollView) {
     ^

my test

struct ScrollableViewTest: View {
    @State private var contentOffset: CGPoint = .zero

    var body: some View {
        ScrollableView(self.$contentOffset, animationDuration: 0.5) {

            Text("Scroll to bottom").onTapGesture {
                self.contentOffset = CGPoint(x: 0, y: 1000)
            }
            ForEach(1...50, id: \.self) { (i : Int) in
                Text("Test \(i)")
            }
            Button(action: {
                self.contentOffset = CGPoint(x: 0, y: 0)
            }) {
                Text("scroll to top")
            }
        }
    }
}

struct ScrollableView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ScrollableViewTest()
        }
    }
}

my deployment target is 13.4
I'm not sure why it's not working. Tried to debug the error messages but I have no clue.

@Mr-Perfection

This comment has been minimized.

Copy link

@Mr-Perfection Mr-Perfection commented Jul 11, 2020

I ended up using https://github.com/Amazd/ScrollViewProxy worked like a charm. Or I think you can create your own scrollView using Introspect pkg. it only worked with static data. When I pass in @ObservedObject (dynamic data) it broke. The issue lies with defining ids before the data is loaded and data being loaded after ids are defined.

@AndAShape

This comment has been minimized.

Copy link

@AndAShape AndAShape commented Jul 20, 2020

I'm doing horizontal scroll with dynamic content. This didn't work. Then today it did. How odd. Thanks. Although I'm still getting the issue with it not rendering correctly until the content is pushed upwards plus I get the Modifying state during view update, this will cause undefined behavior. reported for scrollViewDidScroll error. But not all the time. How does it know that the content has changed?

@Mr-Perfection

This comment has been minimized.

Copy link

@Mr-Perfection Mr-Perfection commented Jul 20, 2020

I'm doing horizontal scroll with dynamic content. This didn't work. Then today it did. How odd. Thanks. Although I'm still getting the issue with it not rendering correctly until the content is pushed upwards plus I get the Modifying state during view update, this will cause undefined behavior. reported for scrollViewDidScroll error. But not all the time. How does it know that the content has changed?

Did it work when you tested on the physical device as well?

@AndAShape

This comment has been minimized.

Copy link

@AndAShape AndAShape commented Jul 21, 2020

My app is so badly put together at present that I cannot tell you this.

@yyl0

This comment has been minimized.

Copy link

@yyl0 yyl0 commented Nov 4, 2020

NavigationLink is not working when clicking on the cell of a view.

did this ever get resolved? @arbnori45

@buluoray

This comment has been minimized.

Copy link

@buluoray buluoray commented Mar 11, 2021

Hi, it seems like it does not support NavigationLink. Is there a workaround?

@macStyle

This comment has been minimized.

Copy link

@macStyle macStyle commented Apr 12, 2021

Is there anyway to implement this with a Tab Bar?
I would like to go to the top of the each page every time I click on the tab item

@lilingxi01

This comment has been minimized.

Copy link

@lilingxi01 lilingxi01 commented May 18, 2021

This ScrollableView works perfectly fine when I want to track the "contentOffset" without using the GeometryReader in SwiftUI (GeometryReader has a lag when other views are animating).

@lilingxi01

This comment has been minimized.

Copy link

@lilingxi01 lilingxi01 commented May 19, 2021

There is a problem in updating UIViewControllerRepresentable when content's @State is changing.

Here is a minimal demo for you to reproduce the problem:

struct ContentView: View {
    @State var offset: CGPoint = .zero
    @State var text = "abc"
    
    var body: some View {
        ScrollableView(self.$offset, animationDuration: 0.5) {
            VStack() {
                Text(text)
            }
        }
        .onAppear {
            self.text = "123"
        }
    }
}

In general, the Text should be modified to "123". But the view cannot update correctly.
And if you make a tiny scroll (either add a long Spacer or allow vertical bounce), the text will be updated.

It seems like a bug in SwiftUI's adaptor for UIView.
Any other people meet this problem?

@jfischoff

This comment has been minimized.

Copy link

@jfischoff jfischoff commented Jul 25, 2021

@lilingxi01 yes I am experiencing this as well

@svenoaks

This comment has been minimized.

Copy link

@svenoaks svenoaks commented Sep 23, 2021

@lilingxi01, @jfischoff I couldn't make your exact demo cause the bug, but I can reproduce with:

struct ContentView: View {
    @State var offset: CGPoint = .zero
    @State var text = "abc"
    
    var body: some View {
        ScrollableView($text, self.$offset, animationDuration: 0.5) {
            VStack() {
                Text(text)
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.text = "123"
            }
            
        }
    }
}

One thing that fixes it is adding a binding to the changing property in the ScrollableView, ie:

let text: Binding<String>

    // MARK: - Init
    init(_ text: Binding<String>, _ offset: Binding<CGPoint>, animationDuration: TimeInterval, showsScrollIndicator: Bool = true, axis: Axis = .vertical, onScale: ((CGFloat)->Void)? = nil, disableScroll: Bool = false, forceRefresh: Bool = false, stopScrolling: Binding<Bool> = .constant(false),  @ViewBuilder content: @escaping () -> Content) {
        self.text = text
@pavm035

This comment has been minimized.

Copy link

@pavm035 pavm035 commented Oct 30, 2021

I think this is something to do with Equatable in ScrollableView if i remove that Equatable it's updating the content, but not sure if there is any other side effect, what i believe is SwiftUI trying to compare view snapshot but due to custom equability it forces that nothing has been changed

@pavm035

This comment has been minimized.

Copy link

@pavm035 pavm035 commented Oct 30, 2021

hmm but NavigationLink is broken, any solution?

@pavm035

This comment has been minimized.

Copy link

@pavm035 pavm035 commented Oct 30, 2021

I think this way NavigationLink worked for me

NavigationView {
            ZStack {
                NavigationLink(destination: Text("Second View"), isActive: $showDetail) { EmptyView() }
                ScrollableView {
                    LazyVStack {
                        ....
                    }
                    .padding()
                }
            }
        }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment