7

我创建了一个符合 UIViewControllerRepresentable 的包装器。我创建了一个 UIViewController,其中包含一个启用了分页的 UIScrollView。自定义包装器可以正常工作。

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

此按钮显示为禁用并显示为灰色。点击它什么都不做。但是,如果将相同的按钮放在 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)
        }
    }
}

“导航链接测试”按钮显示为灰色。 在此处输入图像描述

4

2 回答 2

12

我花了一些时间处理你的代码。我想我了解问题所在,并找到了解决方法。

我认为,问题在于,NavigationLink要启用它,它需要位于NavigationView. 虽然你是,但似乎“连接”与UIHostingController. 如果你检查UIHostingController.navigationController,你会发现它是 nil。

我能想到的唯一解决方案是隐藏可以手动触发的NavigationLink外部(使用其参数)。然后在你的内部,你应该使用一个简单的按钮,当点击它时,改变你的模型来切换绑定。下面是一个似乎可以正常工作的示例。SwiftyUIScrollViewisActiveSwiftyUIScrollViewNavigationLink's isActive

请注意,NavigationLink'sisActive目前有一个小错误,但可能很快就会修复。要了解更多信息: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
        }

    }

}
于 2019-08-14T22:27:23.640 回答
0

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

我遇到了类似的问题。我发现问题出在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
}
}

然后创建一个新类来处理滚动视图的委托。您也可以将以下代码包含到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)
}
}

现在要实现滚动视图,请使用以下代码。

@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
}
于 2020-02-04T07:36:14.067 回答