Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does NavigationLink buttons appear "disabled" in a custom UIViewControllerRepresentable wrapper

I have created a wrapper that conforms to UIViewControllerRepresentable. I have created a UIViewController which contains a UIScrollView that has paging enabled. The custom wrapper works as it should.

SwiftyUIScrollView(.horizontal, pagingEnabled: true) {
      NavigationLink(destination: Text("This is a test")) {
             Text("Navigation Link Test")
      }
}

This button appears disabled and greyed out. Clicking it does nothing. However, if the same button is put inside a ScrollView {} wrapper, it works.

What am I missing here. Here is the custom scrollview class code:

enum DirectionX {
case horizontal
case vertical
}


struct SwiftyUIScrollView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
var axis: DirectionX
var numberOfPages = 0
var pagingEnabled: Bool = false
var pageControlEnabled: Bool = false
var hideScrollIndicators: Bool = false

init(axis: DirectionX, numberOfPages: Int, pagingEnabled: Bool, 
 pageControlEnabled: Bool, hideScrollIndicators: Bool, @ViewBuilder content: 
 @escaping () -> Content) {
    self.content = content
    self.numberOfPages = numberOfPages
    self.pagingEnabled = pagingEnabled
    self.pageControlEnabled = pageControlEnabled
    self.hideScrollIndicators = hideScrollIndicators
    self.axis = axis
}

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

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

class UIScrollViewController: UIViewController, UIScrollViewDelegate {

var axis: DirectionX = .horizontal
var numberOfPages: Int = 0
var pagingEnabled: Bool = false
var pageControlEnabled: Bool = false
var hideScrollIndicators: Bool = false

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

lazy var pageControl : UIPageControl = {
    let pageControl = UIPageControl()
        pageControl.numberOfPages = numberOfPages
        pageControl.currentPage = 0
        pageControl.tintColor = UIColor.white
        pageControl.pageIndicatorTintColor = UIColor.gray
        pageControl.currentPageIndicatorTintColor = UIColor.white
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        pageControl.isHidden = !pageControlEnabled
    return pageControl
}()


var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))

override func viewDidLoad() {
    super.viewDidLoad()

    view.addSubview(scrollView)
    self.makefullScreen(of: self.scrollView, to: self.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)

    view.addSubview(pageControl)
    pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50).isActive = true
    pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    pageControl.heightAnchor.constraint(equalToConstant: 60).isActive = true
    pageControl.widthAnchor.constraint(equalToConstant: 200).isActive = true
}

func makefullScreen(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),
      ])
  }

   func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

        let currentIndexHorizontal = round(scrollView.contentOffset.x / self.view.frame.size.width)
        let currentIndexVertical = round(scrollView.contentOffset.y / self.view.frame.size.height)

    switch axis {
    case .horizontal:
         self.pageControl.currentPage = Int(currentIndexHorizontal)
        break
    case .vertical:
        self.pageControl.currentPage = Int(currentIndexVertical)
        break
    default:
        break
    }

 }

 }

UPDATE

This is how I am using the wrapper:

struct TestData {
var id : Int
var text: String
}


struct ContentView: View {
var contentArray: [TestData] = [TestData(id: 0, text: "Test 1"), TestData(id: 1, text: "Test 2"), TestData(id: 2, text: "TEst 3"), TestData(id: 4, text: "Test 4")]


var body: some View {
    NavigationView {
      GeometryReader { g in
        ZStack{
        SwiftyUIScrollView(axis: .horizontal, numberOfPages: self.contentArray.count, pagingEnabled: true, pageControlEnabled: 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)
            }.frame(width: g.size.width, height: g.size.height)
            .navigationBarTitle("Test")
        }
    }
}
}

struct TestView: View {
var data: TestData
var body: some View {
    GeometryReader { g in
            VStack {
                HStack {
                    Spacer()
                }
                Text(self.data.text)
                Text(self.data.text)

                VStack {
                    NavigationLink(destination: Text("This is a test")) {
                                   Text("Navigation Link Test")
                    }
                }
                Button(action: {
                    print("Do something")
                }) {
                    Text("Button")
                }
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            .background(Color.yellow)
        }
    }
}

The "navigation link test" button is greyed out. enter image description here

like image 328
Osama Naeem Avatar asked Aug 14 '19 18:08

Osama Naeem


1 Answers

I spent some time with your code. I think I understand what the problem is, and found a workaround.

The issue is, I think, that for NavigationLink to be enabled, it needs to be inside a NavigationView. Although yours is, it seems the "connection" is lost with UIHostingController. If you check the UIHostingController.navigationController, you'll see that it is nil.

The only solution I can think of, is having a hidden NavigationLink outside the SwiftyUIScrollView that can be triggered manually (with its isActive parameter). Then inside your SwiftyUIScrollView, you should use a simple button that when tapped, changes your model to toggle the NavigationLink's isActive binding. Below is an example that seems to work fine.

Note that NavigationLink's isActive has a small bug at the moment, but it will probably be fixed soon. To learn more about it: https://swiftui-lab.com/bug-navigationlink-isactive/

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(MyModel()))
import SwiftUI

class MyModel: ObservableObject {
    @Published var navigateNow = false
}

struct TestData {
    var id : Int
    var text: String
}


struct ContentView: View {
    @EnvironmentObject var model: MyModel

    var contentArray: [TestData] = [TestData(id: 0, text: "Test 1"), TestData(id: 1, text: "Test 2"), TestData(id: 2, text: "TEst 3"), TestData(id: 4, text: "Test 4")]


    var body: some View {
        NavigationView {
            GeometryReader { g in
                ZStack{
                    NavigationLink(destination: Text("Destination View"), isActive: self.$model.navigateNow) { EmptyView() }

                    SwiftyUIScrollView(axis: .horizontal, numberOfPages: self.contentArray.count, pagingEnabled: true, pageControlEnabled: 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)
                }.frame(width: g.size.width, height: g.size.height)
                    .navigationBarTitle("Test")
            }
        }
    }
}

struct TestView: View {
    @EnvironmentObject var model: MyModel

    var data: TestData
    var body: some View {

        GeometryReader { g in
            VStack {
                HStack {
                    Spacer()
                }
                Text(self.data.text)
                Text(self.data.text)

                VStack {
                    Button("Pseudo-Navigation Link Test") {
                        self.model.navigateNow = true
                    }
                }
                Button(action: {
                    print("Do something")
                }) {
                    Text("Button")
                }
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                .background(Color.yellow)
        }
    }
}

The other thing is your use of AnyView. It comes with a heavy performance price. It is recommended you only use AnyView with leaf views (not your case). So I did managed to refactor your code to eliminate the AnyView. See below, hope it helps.

import SwiftUI

enum DirectionX {
    case horizontal
    case vertical
}


struct SwiftyUIScrollView<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content
    var axis: DirectionX
    var numberOfPages = 0
    var pagingEnabled: Bool = false
    var pageControlEnabled: Bool = false
    var hideScrollIndicators: Bool = false

    init(axis: DirectionX, numberOfPages: Int,
         pagingEnabled: Bool,
         pageControlEnabled: Bool,
         hideScrollIndicators: Bool,
         @ViewBuilder content: @escaping () -> Content) {

        self.content = content
        self.numberOfPages = numberOfPages
        self.pagingEnabled = pagingEnabled
        self.pageControlEnabled = pageControlEnabled
        self.hideScrollIndicators = hideScrollIndicators
        self.axis = axis
    }

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

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

class UIScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {

    var axis: DirectionX = .horizontal
    var numberOfPages: Int = 0
    var pagingEnabled: Bool = false
    var pageControlEnabled: Bool = false
    var hideScrollIndicators: Bool = false

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

    lazy var pageControl : UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.numberOfPages = numberOfPages
        pageControl.currentPage = 0
        pageControl.tintColor = UIColor.white
        pageControl.pageIndicatorTintColor = UIColor.gray
        pageControl.currentPageIndicatorTintColor = UIColor.white
        pageControl.translatesAutoresizingMaskIntoConstraints = false
        pageControl.isHidden = !pageControlEnabled
        return pageControl
    }()

    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")
    }

    var hostingController: UIHostingController<Content>! = nil

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(scrollView)
        self.makefullScreen(of: self.scrollView, to: self.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)

        view.addSubview(pageControl)
        pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50).isActive = true
        pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        pageControl.heightAnchor.constraint(equalToConstant: 60).isActive = true
        pageControl.widthAnchor.constraint(equalToConstant: 200).isActive = true
    }

    func makefullScreen(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),
        ])
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

        let currentIndexHorizontal = round(scrollView.contentOffset.x / self.view.frame.size.width)
        let currentIndexVertical = round(scrollView.contentOffset.y / self.view.frame.size.height)

        switch axis {
        case .horizontal:
            self.pageControl.currentPage = Int(currentIndexHorizontal)
            break
        case .vertical:
            self.pageControl.currentPage = Int(currentIndexVertical)
            break
        default:
            break
        }

    }

}
like image 125
kontiki Avatar answered Oct 21 '22 05:10

kontiki