Почему кнопки NavigationLink отображаются "отключенными" в пользовательской оболочке UIViewControllerRepresentable

Я создал оболочку, которая соответствует 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)
        }
    }
}

Кнопка "Проверка ссылки навигации" неактивна.

3 ответа

Решение

Я провел некоторое время с вашим кодом. Я думаю, что понимаю, в чем проблема, и нашел обходной путь.

Вопрос в том, я думаю, что для NavigationLink чтобы быть включенным, он должен быть внутри NavigationView, Хотя у вас, кажется, "связь" теряется с UIHostingController, Если вы проверите UIHostingController.navigationController, вы увидите, что это ноль.

Единственное решение, которое я могу придумать, это иметь скрытый NavigationLink вне SwiftyUIScrollView который может быть запущен вручную (с его isActive параметр). Тогда внутри вашего SwiftyUIScrollView, вы должны использовать простую кнопку, которая при нажатии изменяет вашу модель для переключения NavigationLink's isActive связывание. Ниже приведен пример, который работает нормально.

Обратите внимание, что NavigationLink"s isActive в данный момент есть небольшая ошибка, но она, вероятно, будет исправлена ​​в ближайшее время. Чтобы узнать больше об этом: 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)
        }
    }
}

Другое дело, что вы используете AnyView, Это идет с высокой ценой производительности. Рекомендуется использовать только AnyView с видом на лист (не ваш случай). Так что мне удалось реорганизовать ваш код, чтобы устранить AnyView, Смотрите ниже, надеюсь, это поможет.

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
        }

    }

}

У меня была такая же проблема, и я пробовал много разных решений. Навигационная ссылка работала и остановилась. размещение представления внутри навигационного представления сработало.

В примереmasterview()содержит навигационные ссылки, которые не работали, а теперь работают.

      struct ContentView: View {

    var body: some View {
        NavigationView {
            MasterView()
            
            //SettingsView()
            //DetailView()
            //newviewcontroller()
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

Вышеупомянутое решение работает, если нам не требуется переходить на разные экраны из содержимого прокрутки. Однако, если нам нужна навигационная ссылка на содержимое прокрутки вместо самого представления прокрутки, тогда приведенный ниже код будет работать отлично.

У меня была аналогичная проблема. Я понял, что проблема в UIViewControllerRepresentable. Вместо этого используйте UIViewRepresentable, хотя я не уверен, в чем проблема. Мне удалось заставить работать навигационную ссылку, используя приведенный ниже код.

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

Затем создайте новый класс для обработки делегатов scrollview. Вы также можете включить приведенный ниже код в 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
}
Другие вопросы по тегам