为什么NavigationLink按钮在自定义UIViewControllerRepresentable包装器中显示为“禁用”

时间:2019-08-14 18:47:42

标签: uiviewcontroller uiscrollview scrollview swiftui navigationlink

我创建了一个符合UIViewControllerRepresentable的包装器。我创建了一个UIViewController,其中包含一个启用了页面调度的UIScrollView。 自定义包装程序应正常工作。

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

此按钮显示为禁用状态,并显示为灰色。单击它没有任何作用。但是,如果将相同的按钮放在ScrollView {}包装器中,则它将起作用。

我在这里想念什么。这是自定义的scrollview类代码:

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
    }

 }

 }

更新

这就是我使用包装器的方式:

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

“导航链接测试”按钮显示为灰色。 enter image description here

2 个答案:

答案 0 :(得分:2)

我花了一些时间处理您的代码。我想我知道问题出在哪里,并且找到了解决方法。

我认为问题在于,要启用NavigationLink,它必须位于NavigationView内部。尽管是您的,但UIHostingController似乎失去了“连接”。如果您选中UIHostingController.navigationController,则会看到它为空。

我能想到的唯一解决方案是在NavigationLink外面有一个隐藏的SwiftyUIScrollView,可以通过它的isActive参数手动触发它。然后,在SwiftyUIScrollView内,您应该使用一个简单的按钮,在点击该按钮时,更改模型以切换NavigationLink's isActive绑定。下面是一个看起来不错的示例。

请注意,NavigationLink的{​​{1}}目前有一个小错误,但可能很快就会修复。要了解更多信息:https://swiftui-lab.com/bug-navigationlink-isactive/

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) } } } 。它带有沉重的性能价格。建议您仅将AnyView用于叶子视图(而不是大小写)。因此,我确实设法重构了您的代码以消除AnyView。见下文,希望对您有所帮助。

AnyView

答案 1 :(得分:0)

如果不需要我们从滚动视图的内容导航到其他屏幕,则上述解决方案有效。但是,如果我们需要到滚动内容的导航链接,而不是滚动视图本身,那么下面的代码将很完美。

我遇到了类似的问题。我发现问题出在 UIViewControllerRepresentable 。可以使用 UIViewRepresentable ,尽管我不确定问题是什么。我可以使用下面的代码来完成Navigationlink的工作。

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

然后创建一个新类来处理滚动视图的委托。您还可以将以下代码包含在 UIViewRepresentable 中。但是我更喜欢将其分开以得到干净的代码。

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

现在要实现scrollView,请使用以下代码。

@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
}