Skip to content

Instantly share code, notes, and snippets.

@timothycosta
Created July 5, 2019 03:25
Show Gist options
  • Save timothycosta/0d8f64afeca0b6cc29665d87de0d94d2 to your computer and use it in GitHub Desktop.
Save timothycosta/0d8f64afeca0b6cc29665d87de0d94d2 to your computer and use it in GitHub Desktop.
UIScrollView wrapped for SwiftUI
//
// UIScrollViewWrapper.swift
// lingq-5
//
// Created by Timothy Costa on 2019/07/05.
// Copyright © 2019 timothycosta.com. All rights reserved.
//
import SwiftUI
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
@ComethTheNerd
Copy link

Thanks for the gist!

Did you find an issue with an app showing just a black screen if you make the UIScrollViewWrapper full screen (ignoring safe areas)?

It seems to be a problem caused by pin edges here in my experience. If you make the height of the scroll view the full_height - 1 it renders fine (albeit with a 1 pixel gap!)... which is strange!

@timothycosta
Copy link
Author

No, I'm not having that issue. No black screens. This seems to work well:

 	var body: some View {
		GeometryReader { proxy in
			UIScrollViewWrapper {
				VStack {
					ForEach(0..<100, id: \.self) { obj in
						Text("\(obj)")
					}
				}
				.frame(width: proxy.size.width)
				.background(Color.blue.opacity(0.25))
			}
			.edgesIgnoringSafeArea(.all)
		}
	}

@lehelmedves
Copy link

Great wrapper, congrats!
Quick question: when I'm using this wrapper, the scrollview appears correctly, however I am generating its content with a ForEach and when I start adding items into the scrollview, it doesn't update the contentSize correctly - both the beginning and the end of the content starts to fall outside the visible area. Any ideas how this could be fixed or what might cause it?

@carloe
Copy link

carloe commented Feb 26, 2020

@lehelmedves I just ran into the same issue. The sample code above seems to be working for simple views, but for more complex ones the VStack just outside of the ForEach appears to add a bit of extra padding for whatever reason. Setting the spacing to 0 fixed the issue for me:

UIScrollViewWrapper {
  HStack(alignment: .center, spacing: 0) {
    ForEach(0..<100) { _ in
      CellView()
        .frame(width: proxy.size.width, height: proxy.size.height)	
    }
  }
}

@lehelmedves
Copy link

Thanks, I'll try this out!

@wilg
Copy link

wilg commented Mar 29, 2020

I'm having the same issue with the content height. Anybody get it working?

@amkoro
Copy link

amkoro commented May 31, 2020

thank you for wrapper!

but it does not support scrollRectToVisible() ability
tried a different approaches
also I wonder why scrollView.frame, scrollView.contentSize, scrollView.bounds always 0.0 ?

did smb tried to implement such functionality?

@leonboe1
Copy link

Great wrapper, congrats!
Quick question: when I'm using this wrapper, the scrollview appears correctly, however I am generating its content with a ForEach and when I start adding items into the scrollview, it doesn't update the contentSize correctly - both the beginning and the end of the content starts to fall outside the visible area. Any ideas how this could be fixed or what might cause it?

I ran into the same issue. Does somebody know how to fix that?

@timothycosta
Copy link
Author

I don't really recommend this approach anymore. If you want access to the underlying UIScrollView that SwiftUI.ScrollView uses, I would recommend using something like Introspect.

@suryam0801
Copy link

hi how would you use this to scroll to a particular index? can we set the offset manually?

@Flerex
Copy link

Flerex commented May 3, 2021

To anyone that might be using this and found that the subviews added appear on a white background: You just need to add the following line before returning in makeUIViewController:

vc.hostingController.view.backgroundColor = .clear

@rob5408
Copy link

rob5408 commented Jan 19, 2022

When using SwiftUI with tvOS, a ScrollView with a big Text View will not scroll. At least for me it didn't. I used this and added this v.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)] after let v = UIScrollView() and it started working. Many thanks!

@omarabubakrll
Copy link

omarabubakrll commented May 29, 2023

@timothycosta
I fixed the height gap issue for the full height by adjusting the spacing of the VStack to 0:

GeometryReader { proxy in UIScrollViewWrapper { VStack(spacing: 0) { // <--- here ForEach(0..<100, id: \.self) { obj in ZStack { Color.purple Text("\(obj)") } .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) } } .frame(width: proxy.size.width) .background(Color.blue.opacity(0.25)) } .ignoresSafeArea() }

@dz902
Copy link

dz902 commented Apr 13, 2024

Works great, just that if I put VStack in, VStack width is bound to its content and not greedy.

@mysterytoy
Copy link

I'm having the same issue with the content height. Anybody get it working?

@wilg Did you ever get this working?

@wilg
Copy link

wilg commented Apr 17, 2024

no, sorry

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment