When you start developing with SwiftUI, you will reach certain points during development where you would like to use some APIs from UIKit. In order to integrate UIKit with SwiftUI, you can use one of two protocols – UIViewControllerRepresentable and UIViewRepresentable. We will soon be migrating all of our iPhone app templates (currently running on UIKit) to SwiftUI, so having them use both UIKit and SwiftUI as a transition state during migration will be a huge advantage in order to not block our customers who are willing to take a stab at SwiftUI early on (even if we don’t recommend it yet!).

UIKit swiftui

The main difference between the two protocols is that UIViewControllerRepresentable is used to represent a UIKit view controller and UIViewRepresentable can be used to represent a UIKit view in your SwiftUI project.

We will use UIViewControllerRepresentable to create a custom ScrollView wrapper. Let’s say we want to create a scrollView that contains Pagination. The default ScrollView that comes with SwiftUI doesn’t have this feature – so we can create a UIViewController that houses a ScrollView with pagination enabled. Then we can integrate this custom UIViewController with our SwiftUI content.

Let’s start by first creating a simple UIViewController

class UIScrollViewController<Content: View>: UIViewController

Then we will create a simple scrollView like so:

lazy var scrollView: UIScrollView = {
        let view = UIScrollView()
        view.delegate = self
        view.isPagingEnabled = pagingEnabled
        view.showsVerticalScrollIndicator = !hideScrollIndicators
        view.showsHorizontalScrollIndicator = !hideScrollIndicators
        return view
    }()


 var hostingController: UIHostingController<Content>! = nil

 init(rootView: Content) {
        self.hostingController = UIHostingController<Content>(rootView: rootView)
        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

We have created a scrollView, and also created a hostingController property. This UIHostingController is basically initialized by the content that we have wrapped this wrapper around. So that’s what we have done in the init block. ( more on this later) 

Make sure to add the scrollView as a subview and assign the constraints using auto layout.

override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(scrollView)
        self.makefullScreen(of: self.scrollView, to: self.view)    //assiging autolayout to scrollview

        // add hostingController as child VC and laying out constraints for the child view controller’s view
        self.hostingController.willMove(toParent: self)
        self.scrollView.addSubview(self.hostingController.view)
        self.makefullScreen(of: self.hostingController.view, to: self.scrollView)                
        self.hostingController.didMove(toParent: self)
}

Once the ViewController is set up. Now it’s time to implement the protocol. Start by first constructing a struct – I have named mine as SwiftyUIScrollView. It conforms to UIViewControllerRepresentable, so we get methods to which I need to conform to. 

The struct has an initializer with one mandatory closure property called content. The remaining are something that you can add yourself. I have added two more including pagingEnabled bool value.

struct SwiftyUIScrollView<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content
    var pagingEnabled: Bool = false
    var hideScrollIndicators: Bool = false
        
    init(pagingEnabled: Bool,
         hideScrollIndicators: Bool,
         @ViewBuilder content: @escaping () -> Content) {

        self.content = content
        self.pagingEnabled = pagingEnabled
        self.hideScrollIndicators = hideScrollIndicators
     }

    func makeUIViewController(context: Context) -> UIScrollViewController<Content> {
        let vc = UIScrollViewController(rootView: self.content())
        vc.pagingEnabled = pagingEnabled
        vc.hideScrollIndicators = hideScrollIndicators
        return vc
    }

    func updateUIViewController(_ viewController: UIScrollViewController<Content>, context: Context) {
        viewController.hostingController.rootView = self.content()
    }
}

Once the initializer is done, we will move to the second method where we will create a UIScrollViewController and return that view controller. We will create a new instance of UIScrollViewController and initialize it with the content. Then we will assign values that we got from the initializer to the properties in the view controller and return the controller. 

And that’s about it. Now we can simply use this wrapper where ever we want to show a scrollView and using the initializer property – enable or disable the pagination feature.

This is how you can use this custom wrapper now: 

SwiftyUIScrollView(pagingEnabled: true, hideScrollIndicators: true) {
                    HStack(spacing: 0) {
                        ForEach(self.contentArray, id: \.id) { item in
                            TestView(data: item)
                                .frame(width: g.size.width, height: g.size.height)
                        }
                    }
            }.frame(width: g.size.width)

The content closure contains a HStack – all this content is assigned to the hostingViewController that is added as child VC to our custom UIScrollViewController and the child’s view is added as a subview to our scrollView. So now the content automatically sizes up the scrollView and since the scrollView has pagination enabled we get the snap effect when scrolling through.


Leave a Reply

Your email address will not be published. Required fields are marked *