Как работать с NavigationLink внутри UIViewControllerRepresentable оболочки?

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

Эти кнопки NavigationLink не работают.

scrollViewWrapper находится внутри NavigationView. Я создал тестовую кнопку, которая является простой кнопкой, и она, кажется, работает. Так что есть что-то, что я не правильно делаю с NavigationLink и кастомными UIViewControllerRepresentable,

Здесь я использую пользовательскую оболочку.

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

Внешний вид:

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

Пользовательский код ScrollView Wrapper:

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

5 ответов

Как указывалось в других ответах, существует проблема с помещением NavigationLink внутри UIViewControllerRepresentable.

Я решил это, заключив свои UIViewControllerRepresentable и NavigationLink в представление и программно активировав NavigationLink из UIViewControllerRepresentable.

Например:

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

В моем случае я передал экземпляр MyView в UIViewController, который оборачивается моим MyViewControllerRepresentable, и назвал мой goTo(destination:AnyView) при нажатии кнопки.

Разница между нашими случаями в том, что мой UIViewController был моим собственным классом, написанным с помощью UIKit (по сравнению с UIHostingController). В случае, если вы используете UIHostingController, вы, вероятно, могли бы использовать общий ObservableObject, содержащийdestination а также is_activeпеременные. Вы бы изменили свои 2 ссылки NavigationLinks на кнопки, чтобы методы действия изменяли ObservableObjectdestination а также is_active переменные.

Это расширение к вышеупомянутому решению, которое никогда не прокручивает внутреннее содержимое.

У меня была аналогичная проблема. Я понял, что проблема в 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
}

Это происходит потому, что вы используете UIViewControllerRepresentable вместо UIViewRepresentable. Я предполагаю, что UIScrollViewController предотвращает представление целевого контроллера текущим контроллером.

Вместо этого попробуйте приведенный выше код:

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

Не забудьте добавить свой хостинг-контроллер в детстве.

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

Вы установили запущенные сегы? Если вы используете XCode, вы можете щелкнуть правой кнопкой мыши на кнопку, которую вы создали в основной раскадровке. Если он не настроен, вы можете перейти к инспектору соединений в верхней правой боковой панели, где вы можете найти Инспектор файлов, Инспектор идентификации, Инспектор атрибутов... и указать действие, которое должна выполнять ваша кнопка.

Другие вопросы по тегам