How to handle NavigationLink inside UIViewControllerRepresentable wrapper?
Asked Answered
E

5

6

So I am trying to create a custom pagination scrollView. I have been able to create that wrapper and the content inside that wrapper consists of a custom View. Inside that custom View i have got two NavigationLink buttons when pressed should take users to two different Views.

Those NavigationLink buttons are not working.

The scrollViewWrapper is inside a NavigationView. I created a test button which is just a simple Button and that seems to work. So there is something that I am not doing correctly with NavigationLink and custom UIViewControllerRepresentable.

This is where I am using the custom wrapper.

NavigationView {
            UIScrollViewWrapper {
                HStack(spacing: 0) {
                    ForEach(self.onboardingDataArray, id: \.id) { item in
                          OnboardingView(onboardingData: item)
                                .frame(width: geometry.size.width, height: geometry.size.height)
                       }
                    }
            }.frame(width: geometry.size.width, height: geometry.size.height)
             .background(Color.blue)
           }

The onboarding view:

struct OnboardingView: View {
var onboardingData: OnboardingModel

var body: some View {
    GeometryReader { geometry in
        VStack(spacing: 10) {
            Spacer()
            Image("\(self.onboardingData.image)")
                .resizable()
                .frame(width: 300, height: 300)
                .aspectRatio(contentMode: ContentMode.fit)
                .clipShape(Circle())
                .padding(20)

            Text("\(self.onboardingData.titleText)")
                .frame(width: geometry.size.width, height: 20, alignment: .center)
                .font(.title)

            Text("\(self.onboardingData.descriptionText)")
                .lineLimit(nil)
                .padding(.leading, 15)
                .padding(.trailing, 15)
                .font(.system(size: 16))
                .frame(width: geometry.size.width, height: 50, alignment: .center)
                .multilineTextAlignment(.center)
            Spacer(minLength: 20)
            if self.onboardingData.showButton ?? false {
                VStack {
                    Button(action: {
                        print("Test")
                    }) {
                        Text("Test Button")
                    }
                    NavigationLink(destination: LogInView()) {
                        Text("Login!")
                    }
                    NavigationLink(destination: SignUpView()) {
                        Text("Sign Up!")
                    }
                }
            }

            Spacer()
        }
    }
}
    }

The custom ScrollView Wrapper code:

struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
}

func makeUIViewController(context: Context) -> UIScrollViewController {
    let vc = UIScrollViewController()
    vc.hostingController.rootView = AnyView(self.content())
    return vc
}

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

class UIScrollViewController: UIViewController {

lazy var scrollView: UIScrollView = {
    let view = UIScrollView()
    view.isPagingEnabled = true
    return view
}()

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),
      ])
  }
Engineer answered 14/8, 2019 at 12:39 Comment(3)
I have exactly the same question did you make any progress ?Clishmaclaver
Any progress regarding this issue??Hammel
Try the solution that I have written, it would definitely work.Romeyn
M
1

As the other answers have stated, there is an issue with putting the NavigationLink inside the UIViewControllerRepresentable.

I solved this by wrapping my UIViewControllerRepresentable and a NavigationLink inside a View and programmatically activating the NavigationLink from inside the UIViewControllerRepresentable.

For example:

struct MyView: View
{        
    @State var destination: AnyView? = nil
    @State var is_active: Bool = false

    var body: some View
    {
        ZStack
        {
            MyViewControllerRepresentable( self )

            NavigationLink( destination: self.destination, isActive: self.$is_active )
            {
                EmptyView()
            }
        }
    }

    func goTo( destination: AnyView )
   {
        self.destination = destination
        self.is_active = true
   }
}

In my case, I passed the MyView instance to the UIViewController that my MyViewControllerRepresentable is wrapping, and called my goTo(destination:AnyView) method when a button was clicked.

The difference between our cases is that my UIViewController was my own class written with UIKit (compared to a UIHostingController). In the case that you're using a UIHostingController, you could probably use a shared ObservableObject containing the destination and is_active variables. You'd change your 2 NavigationLinks to Buttons having the action methods change the ObservableObject's destination and is_active variables.

Misnomer answered 2/3, 2020 at 7:8 Comment(1)
how do you call the goTo function from the UIViewControllerRepresentable?Hindustan
D
0

This happens because you use a UIViewControllerRepresentable instead of UIViewRepresentable. I guess the UIScrollViewController keeps the destination controller from being presented by the current controller.

Try the code above instead:

import UIKit
import SwiftUI

struct ScrollViewWrapper<Content>: UIViewRepresentable where Content: View{
    func updateUIView(_ uiView: UIKitScrollView, context: UIViewRepresentableContext<ScrollViewWrapper<Content>>) {

    }


    typealias UIViewType = UIKitScrollView

    let content: () -> Content
    var showsIndicators : Bool

    public init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, @ViewBuilder content: @escaping () -> Content) {
        self.content = content
        self.showsIndicators = showsIndicators

    }

    func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIViewType {
        let hosting = UIHostingController(rootView: AnyView(content()))
        let width = UIScreen.main.bounds.width
        let size = hosting.view.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
        hosting.view.frame = CGRect(x: 0, y: 0, width: width, height: size.height)
        let view = UIKitScrollView()
        view.delegate = view
        view.alwaysBounceVertical = true
        view.addSubview(hosting.view)
        view.contentSize = CGSize(width: width, height: size.height)
        return view
    }

}


class UIKitScrollView: UIScrollView, UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print(scrollView.contentOffset) // Do whatever you want.
    }
}
Dittography answered 15/11, 2019 at 19:55 Comment(0)
R
0

This is an extension to the above solution that never scrolls the inner content.

I was into a similar problem. I have figured out that the problem is with the UIViewControllerRepresentable. Instead use UIViewRepresentable, although I am not sure what the issue is. I was able to get the navigationlink work using the below code.

struct SwiftyUIScrollView<Content>: UIViewRepresentable where Content: View {
typealias UIViewType = Scroll

var content: () -> Content
var pagingEnabled: Bool = false
var hideScrollIndicators: Bool = false
@Binding var shouldUpdate: Bool
@Binding var currentIndex: Int

var onScrollIndexChanged: ((_ index: Int) -> Void)

public init(pagingEnabled: Bool,
            hideScrollIndicators: Bool,
            currentIndex: Binding<Int>,
            shouldUpdate: Binding<Bool>,
            @ViewBuilder content: @escaping () -> Content, onScrollIndexChanged: @escaping ((_ index: Int) -> Void)) {
    self.content = content
    self.pagingEnabled = pagingEnabled
    self._currentIndex = currentIndex
    self._shouldUpdate = shouldUpdate
    self.hideScrollIndicators = hideScrollIndicators
    self.onScrollIndexChanged = onScrollIndexChanged
}

func makeUIView(context: UIViewRepresentableContext<SwiftyUIScrollView>) -> UIViewType {
    let hosting = UIHostingController(rootView: content())
    let view = Scroll(hideScrollIndicators: hideScrollIndicators, isPagingEnabled: pagingEnabled)
    view.scrollDelegate = context.coordinator
    view.alwaysBounceHorizontal = true
    view.addSubview(hosting.view)
    makefullScreen(of: hosting.view, to: view)
    return view
}

class Coordinator: NSObject, ScrollViewDelegate {
    func didScrollToIndex(_ index: Int) {
        self.parent.onScrollIndexChanged(index)
    }

    var parent: SwiftyUIScrollView

    init(_ parent: SwiftyUIScrollView) {
        self.parent = parent
    }
}

func makeCoordinator() -> SwiftyUIScrollView<Content>.Coordinator {
    Coordinator(self)
}

func updateUIView(_ uiView: Scroll, context: UIViewRepresentableContext<SwiftyUIScrollView<Content>>) {
    if shouldUpdate {
        uiView.scrollToIndex(index: currentIndex)
    }
}

func makefullScreen(of childView: UIView, to parentView: UIView) {
    childView.translatesAutoresizingMaskIntoConstraints = false
    childView.leftAnchor.constraint(equalTo: parentView.leftAnchor).isActive = true
    childView.rightAnchor.constraint(equalTo: parentView.rightAnchor).isActive = true
    childView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
    childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
}
}

Then create a new class to handle the delegates of a scrollview. You can include the below code into the UIViewRepresentable as well. But I prefer keeping it separated for a clean code.

class Scroll: UIScrollView, UIScrollViewDelegate {

var hideScrollIndicators: Bool = false
var scrollDelegate: ScrollViewDelegate?
var tileWidth = 270
var tileMargin = 20

init(hideScrollIndicators: Bool, isPagingEnabled: Bool) {
    super.init(frame: CGRect.zero)
    showsVerticalScrollIndicator = !hideScrollIndicators
    showsHorizontalScrollIndicator = !hideScrollIndicators
    delegate = self
    self.isPagingEnabled = isPagingEnabled
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
    scrollDelegate?.didScrollToIndex(Int(currentIndex))
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let currentIndex = scrollView.contentOffset.x / CGFloat(tileWidth+tileMargin)
    scrollDelegate?.didScrollToIndex(Int(currentIndex))
}

func scrollToIndex(index: Int) {
    let newOffSet = CGFloat(tileWidth+tileMargin) * CGFloat(index)
    contentOffset = CGPoint(x: newOffSet, y: contentOffset.y)
}
}

Now to implement the scrollView use the below code.

@State private var activePageIndex: Int = 0
@State private var shouldUpdateScroll: Bool = false

SwiftyUIScrollView(pagingEnabled: false, hideScrollIndicators: true, currentIndex: $activePageIndex, shouldUpdate: $shouldUpdateScroll, content: {
            HStack(spacing: 20) {
                ForEach(self.data, id: \.id) { data in
                    NavigationLink(destination: self.getTheNextView(data: data)) {
                        self.cardView(data: data)
                    }
                }
            }
            .padding(.horizontal, 30.0)
        }, onScrollIndexChanged: { (newIndex) in
           shouldUpdateScroll = false
           activePageIndex = index
            // Your own required handling
        })


func getTheNextView(data: Any) -> AnyView {
    // Return the required destination View
}
Romeyn answered 4/2, 2020 at 7:28 Comment(0)
C
0

Don't forget to add your hosting controller as a child.

override func viewDidLoad() {
    super.viewDidLoad()

    self.view.addSubview(self.scrollView)
    self.pinEdges(of: self.scrollView, to: self.view)
    addChild(self.hostingController)
    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)
}
Celestyna answered 3/9, 2021 at 7:13 Comment(0)
H
-2

Did you set up the Triggered Segues? If you are using Xcode you can right click the button you created in the main storyboard. If it's not set up you can go to the connections inspector on the top right sidebar where you can find the File inspector,Identity inspector, Attributes inspector... and specify the action of what you want your button to do.

Hasin answered 14/8, 2019 at 12:54 Comment(1)
I am using SwiftUI here.Engineer

© 2022 - 2024 — McMap. All rights reserved.