UIStackView 或 UITableView 内的 UIPageViewController [已关闭]

UIPageViewController inside a UIStackView or UITableView [closed]

提问人:Shawn Frank 提问时间:9/11/2023 最后编辑:Shawn Frank 更新时间:9/17/2023 访问量:83

问:


想改进这个问题吗?更新问题,使其仅通过编辑这篇文章来关注一个问题。

2个月前关闭。

我正在尝试使用 UIKit 和 Storyboard 创建入职 UI。我有一个要求,即某些部分需要静态/粘性,而某些部分必须在用户在入职时水平滚动。

这应该给你一个更好的主意:

Onboarding swift UIPageViewController in UIStackView UIScrollView UITableView Autolayout

为了实现上述目的,我使用了在情节提要中设置的以下视图层次结构

UIView控制器

  • 跳过按钮
  • 包含用于水平滑动的 UIPageViewController 的容器视图
    • UIView控制器
      • UIScrollView(这是另一个滚动视图,而不是页面视图控制器的)
        • UIStack视图
          • UIImage(大红十字)
          • UILabel的
          • UILabel的
  • UIStack视图
  • 页面指示器
  • “订阅”按钮
  • 登录按钮

虽然这在正常设置中工作正常,但我需要支持用户可以从设置/控制面板更新的动态/大文本。

我现在遇到的问题是内容在UIPageViewController内的滚动视图中增长,如图所示

UIStackView UIScrollView UIPageController Swift Onboarding

上面的白框不是 UI 的一部分,而只是我试图表明垂直滚动仅限于该区域。

我明白,由于我的设置,这是正确的行为。

我的问题/目标: 什么是更好的布局结构,以便在激活较大的文本字体时整个视图增长,从而使视图作为一个整体滚动,而不是滚动被限制在屏幕的一小部分,这意味着页面指示器和底部按钮位于屏幕下方以获得更大的文本大小。

请记住,只有图片、标题和描述是可水平滚动的。

我试图将 scrollview 和 stackview 从 PageViewController 中取出,以设置如下内容:

UIView控制器

  • UIScroll视图
    • UIStack视图
      • 跳过按钮
      • 包含用于水平滑动的 UIPageViewController 的容器视图
        • UIView控制器
          • UIStack视图
            • UIImage(大红十字)
            • UILabel的
            • UILabel的
      • UIStack视图
        • 页面指示器
        • “订阅”按钮
        • 登录按钮

但是,由于我使用的是自动布局,因此我在情节提要中收到一个错误,指出无法确定页面视图控制器的高度。

如果无法通过故事板或程序化设置,那么在故事板中或以编程方式设置它的正确方法是什么?

如果这样可以让生活更轻松,我愿意切换到 tableview 或 collectionview。

iOS Swift UIscrollView 自动布局 UISkackView

评论

0赞 DonMag 9/12/2023
这似乎更像是一个设计问题,而不是代码/布局管理问题。例如,你真的想让你的“入职 1”标签字体增长到那个大小吗?也就是说,看到这个词像这样被破坏会是“用户友好”的吗?您是否嵌入垂直滚动视图,因为您的“描述”可能会有很多行?仅垂直滚动文本是否更有意义?
0赞 Shawn Frank 9/12/2023
@DonMag 标题和描述可能是多行是的,但是,我们尽量将其保持在最多 1-2 行,以避免滚动。滚动视图的主要原因是当用户从设置/控制中心选择更大的文本大小时,允许文本大小增长。如果文本大小设置为 150 - 300%,则需要滚动文本。与其只是滚动文本,我更希望整个视图是可滚动的。
0赞 DonMag 9/13/2023
这离你的目标很近吗?i.stack.imgur.com/E7H0d.png......页面视图控制器框架具有恒定的高度,而不管其中的垂直滚动文本如何?
0赞 Shawn Frank 9/13/2023
@DonMag - 谢谢你的插图,这几乎是我想要的。我想删除 PageVC 中的滚动视图,以便 PageVC 可以根据内容增长。唯一的滚动视图是外部的滚动视图,它用黄色标记,以便整个视图在安全区域中滚动。当 PageVC 中的内容增长时,按钮等将在插图的黄色滚动视图中进一步向下推。希望这是有道理的吗?
0赞 DonMag 9/13/2023
因此,您希望 PageVC 高度根据页面内容而变化......当用户在页面之间滑动时,按钮应该会发生什么情况?这让我们回到了设计问题......看看这张图片 - 我想你会明白我的意思(真的,非常大的图片):i.stack.imgur.com/wVfvB.jpg

答:

1赞 DonMag 9/16/2023 #1

经过评论讨论,我想我理解了你的最终目标。

您正在创建一个“入职”UI,因此我将做出一些假设......

  • 可能不是几十个“页面”
  • 可能是相当“静态”的页面(每个“页面”上没有或很少用户交互)

因此,如果“页面”不是动态分配的,我们不必担心内存问题。

A 是一个不错的组件 -- 但是,它在“幕后”做了很多工作。特别是,它设置“页面框架”以匹配其框架。在您的例子中,您希望框架的高度增加以匹配页面高度。UIPageViewController

这可能是可以做到的,但不使用页面视图控制器会容易得多。

相反,让我们使用标准的 .UIScrollView


首先,如果我们在 中嵌入 ,并将视图的 Height 约束等于标签的 Height(假设我们从表到视图有 8 点约束):UILabelUIView

someView.heightAnchor.constraint(equalTo: someLabel.heightAnchor, constant: 16.0).isActive = true

黄色视图的高度将随着标签的增长而增加:

enter image description here

我们可以用一个(红色虚线的轮廓是滚动视图框架)做同样的事情:UIScrollView

enter image description here

滚动视图框架 Height 随着标签高度的增加而增长,我们仍然可以水平滚动,但不能垂直滚动。


因此,让我们首先将“页面”放入水平堆栈视图中:

enter image description here

,然后将该堆栈视图嵌入到滚动视图中。

如果我们将每个页面的 Width 限制为滚动视图的 Width,并在滚动视图上设置一个 Height 约束,我们将得到这个(粗黑色虚线轮廓是滚动视图框架):

enter image description here

到目前为止,没什么特别的。

但是,如果页面高度发生变化 - 例如在标签上使用 - 而不做任何其他操作 - 我们会得到这个:.adjustsFontForContentSizeCategory - true

enter image description here

enter image description here

这会导致垂直和水平滚动 - 正如预期的那样。

但是,对于目标布局,我们希望滚动视图的 Height 增长以匹配新的页面 Height

让我们将滚动视图的 Height 限制为堆栈视图的 Height:

    NSLayoutConstraint.activate([
        // "normal" Top/Leading/Trailing constraints
        pageScrollView.topAnchor.constraint(equalTo: view.topAnchor),
        pageScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        pageScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        
        // instead of setting a Height related to the view, or a constant value
        //  we'll constrain the Height to the *stack view's* height
        pageScrollView.heightAnchor.constraint(equalTo: pageStackView.heightAnchor),
    ])

现在,随着“页面”高度的增加,滚动视图框架也会增长:

enter image description here

enter image description here

一旦我们将我们的嵌入到“外部”滚动视图中,并相对于其他“外部”UI 元素对其进行约束,我们将到达目标布局。pageScrollView

纯黑色轮廓是“iPhone”框架;黄色长虚线轮廓是“外部”滚动视图框架;白色虚线轮廓是“页面”滚动视图框架:

enter image description here

随着“页面”高度的增长......

enter image description here

我们将能够垂直滚动整个“外部视图”......

enter image description here

并继续水平滚动“页面”。

enter image description here

enter image description here

最终结果 - (按比例缩小,以便在此处发布):

enter image description here

最后,一些示例代码。一切都是通过代码完成的,因此不需要复杂的故事板设计或/或连接:@IBOutlet@IBAction


“页面”数据的简单结构:

struct PageData {
    var title: String = ""
    var desc: String = ""
    
    // maybe different images on each page?
    var imgName: String = ""
}

示例“页面”视图控制器 - 我们将将其添加为子项并获取视图。ImageView、Title 和 Description 标签:

class SamplePageVC: UIViewController {
    
    let titleLabel: UILabel = UILabel()
    let descLabel: UILabel = UILabel()
    let imgView: UIImageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .clear
        
        guard let customFont1 = UIFont(name: "TimesNewRomanPSMT", size: 32.0),
              let customFont2 = UIFont(name: "Verdana", size: 16.0)
        else {
            fatalError("Could not load font!")
        }
        
        titleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont1)
        descLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont2)
        
        [titleLabel, descLabel].forEach { v in
            v.adjustsFontForContentSizeCategory = true
            v.textAlignment = .center
            v.textColor = .white
            v.numberOfLines = 0
            v.setContentHuggingPriority(.required, for: .vertical)
            v.setContentCompressionResistancePriority(.required, for: .vertical)
        }
        
        // might want to set a MAX Content Size Category?
        //view.maximumContentSizeCategory = .accessibilityExtraLarge
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.spacing = 6
        
        [imgView, titleLabel, descLabel].forEach { v in
            stackView.addArrangedSubview(v)
        }
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        let g = view.safeAreaLayoutGuide
        let bc: NSLayoutConstraint = stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0)
        bc.priority = .required - 1
        
        NSLayoutConstraint.activate([
            
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            stackView.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: -8.0),
            bc,
            
            imgView.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -60.0),
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0),
            
            titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -20.0),
            descLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -20.0),
            
        ])
    }
    
}

示例“载入”视图控制器 - 使用上面讨论的所有内容:

class OnboardingVC: UIViewController, UIScrollViewDelegate {
    
    let skipBtn: UIButton = UIButton()
    let subscribeBtn: UIButton = UIButton()
    let loginBtn: UIButton = UIButton()
    
    let pgCtrl: UIPageControl = UIPageControl()
    
    let outerScrollView: UIScrollView = UIScrollView()
    let outerContentView: UIView = UIView()
    
    let pageScrollView: UIScrollView = UIScrollView()
    let pageStackView: UIStackView = UIStackView()
    
    var pageData: [PageData] = [
        PageData(title: "First Page", desc: "Some description about it.", imgName: "pgXC"),
        PageData(title: "Short", desc: "This page has somewhat longer description text.", imgName: ""),
        PageData(title: "A Longer Title", desc: "This page will have even more text in the description label. That will help demonstrate the height matching and resulting layout / scrolling changes.", imgName: ""),
        PageData(title: "Final", desc: "This is the last page.", imgName: ""),
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0)
        
        guard let skipFont = UIFont(name: "Verdana", size: 17.0),
              let btnFont = UIFont(name: "Verdana-Bold", size: 16.0)
        else {
            fatalError("Could not load font!")
        }

        skipBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: skipFont)
        subscribeBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: btnFont)
        loginBtn.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: btnFont)

        for (btn, str) in zip([skipBtn, subscribeBtn, loginBtn], ["Skip", "Subscribe", "Login"]) {
            btn.titleLabel?.adjustsFontForContentSizeCategory = true
            btn.setTitle(str, for: [])
            btn.layer.cornerRadius = 6
        }
        
        skipBtn.setTitleColor(.white, for: .normal)
        skipBtn.setTitleColor(.lightGray, for: .highlighted)
        skipBtn.setContentCompressionResistancePriority(.required, for: .vertical)
        
        subscribeBtn.setTitleColor(.blue, for: .normal)
        subscribeBtn.setTitleColor(.systemBlue, for: .highlighted)
        subscribeBtn.backgroundColor = .white
        
        loginBtn.setTitleColor(.white, for: .normal)
        loginBtn.setTitleColor(.lightGray, for: .highlighted)
        loginBtn.layer.borderColor = UIColor.white.cgColor
        loginBtn.layer.borderWidth = 1
        
        let btnStack: UIStackView = UIStackView()
        btnStack.axis = .vertical
        btnStack.spacing = 12
        
        // let's add top and bottom padding for the buttons
        //  we're not using UIButtonConfiguration so we ignore the deprecation warnings
        [subscribeBtn, loginBtn].forEach { v in
            var edges = v.contentEdgeInsets
            edges.top = 12.0
            edges.bottom = 12.0
            v.contentEdgeInsets = edges
        }
        
        // add Page Control and bottom buttons to vertical stack view
        //  set Hugging and Compression priorities to .required so they
        //  won't stretch or collapse vertically
        [pgCtrl, subscribeBtn, loginBtn].forEach { v in
            btnStack.addArrangedSubview(v)
            v.setContentHuggingPriority(.required, for: .vertical)
            v.setContentCompressionResistancePriority(.required, for: .vertical)
        }
        
        // add "pages" stack view to "page" scroll view
        pageStackView.translatesAutoresizingMaskIntoConstraints = false
        pageScrollView.addSubview(pageStackView)
        
        // add elements to outerContentView
        [skipBtn, pageScrollView, btnStack].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            outerContentView.addSubview(v)
        }
        
        // add outerContentView to outerScrollView
        outerContentView.translatesAutoresizingMaskIntoConstraints = false
        outerScrollView.addSubview(outerContentView)
        
        // add outerScrollView to (self) view
        outerScrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(outerScrollView)
        
        let g = view.safeAreaLayoutGuide
        
        var cg = pageScrollView.contentLayoutGuide
        var fg = pageScrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // constrain all 4 sides of pageStackView to pageScrollView.contentLayoutGuide
            pageStackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            pageStackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            pageStackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            pageStackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
            
            // constrain skip button Top/Trailing
            skipBtn.topAnchor.constraint(equalTo: outerContentView.topAnchor, constant: 12.0),
            skipBtn.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: -40.0),
            
            // constrain pageScrollView
            //  Top 8-points below skip button Bottom
            //  Leading/Trailing to outerContentView (so, full width)
            pageScrollView.topAnchor.constraint(equalTo: skipBtn.bottomAnchor, constant: 8.0),
            pageScrollView.leadingAnchor.constraint(equalTo: outerContentView.leadingAnchor, constant: 0.0),
            pageScrollView.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: 0.0),
            
            // constrain pageScrollView HEIGHT to pageStackView HEIGHT
            //  now, the scroll view Height will match the "pages" height
            pageScrollView.heightAnchor.constraint(equalTo: pageStackView.heightAnchor, constant: 0.0),
            
            // constrain btnStack
            //  Top >= pageScrollView Bottom plus a little "padding space"
            //  Leading/Trailing to outerContentView plus a little "padding space" on the sides
            //  Bottom to outerContentView plus a little "padding space"
            btnStack.topAnchor.constraint(greaterThanOrEqualTo: pageScrollView.bottomAnchor, constant: 12.0),
            btnStack.leadingAnchor.constraint(equalTo: outerContentView.leadingAnchor, constant: 20.0),
            btnStack.trailingAnchor.constraint(equalTo: outerContentView.trailingAnchor, constant: -20.0),
            btnStack.bottomAnchor.constraint(equalTo: outerContentView.bottomAnchor, constant: -12.0),
            
        ])
        
        cg = outerScrollView.contentLayoutGuide
        fg = outerScrollView.frameLayoutGuide
        
        // we want outerContentView to be the same Height as outerScrollView
        //  but less-than-required Priority so it can grow based on its subviews
        let hc: NSLayoutConstraint = outerContentView.heightAnchor.constraint(equalTo: fg.heightAnchor, constant: 0.0)
        hc.priority = .required - 1
        
        NSLayoutConstraint.activate([
            
            // constrain all 4 sides of outerContentView to outerScrollView.contentLayoutGuide
            outerContentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
            outerContentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
            outerContentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
            outerContentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
            
            // outerContentView Width to outerScrollView.frameLayoutGuide Width
            //  so we will never get horizontal scrolling
            outerContentView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: 0.0),
            hc,
            
            // constrain all 4 sides of outerScrollView to (self) view
            outerScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            outerScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            outerScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            outerScrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
        ])
        
        // add page view VCs for each data item
        //  we could do this with a "SamplePageView" UIView subclass
        //  but this shows how to use UIViewController if that's how the "pages" are setup
        
        for (idx, d) in pageData.enumerated() {
            
            let vc = SamplePageVC()
            
            addChild(vc)
            
            // add its view to pageStackView
            pageStackView.addArrangedSubview(vc.view)
            
            vc.didMove(toParent: self)
            
            vc.titleLabel.text = d.title
            vc.descLabel.text = d.desc
            
            if !d.imgName.isEmpty, let img = UIImage(named: d.imgName) {
                vc.imgView.image = img
            } else if let img = UIImage(systemName: "\(idx).circle.fill") {
                vc.imgView.image = img
                vc.imgView.tintColor = .orange
            }
            
            // each page view Width is equal to pageScrollView.frameLayoutGuide width
            vc.view.widthAnchor.constraint(equalTo: pageScrollView.frameLayoutGuide.widthAnchor, constant: 0.0).isActive = true
            
        }
        
        // we *probably* do not want to see scroll indicators
        outerScrollView.showsHorizontalScrollIndicator = false
        outerScrollView.showsVerticalScrollIndicator = false
        pageScrollView.showsHorizontalScrollIndicator = false
        pageScrollView.showsVerticalScrollIndicator = false
        
        // enable paging for the "pages"
        pageScrollView.isPagingEnabled = true
        
        // we will implement scrollViewDidScroll() so we can update the page control
        //  when the user drags the pages left/right
        pageScrollView.delegate = self
        
        pgCtrl.numberOfPages = pageData.count
        pgCtrl.addTarget(self, action: #selector(changePage(_:)), for: .valueChanged)
        
    }
    
    @objc func changePage(_ sender: UIPageControl) {
        let x: CGFloat = pageScrollView.frame.width * CGFloat(sender.currentPage)
        UIView.animate(withDuration: 0.3, animations: {
            self.pageScrollView.contentOffset.x = x
        })
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == pageScrollView {
            let pg: Int = Int(floor((scrollView.contentOffset.x + scrollView.frame.width * 0.5) / scrollView.frame.width))
            pgCtrl.currentPage = pg
        }
    }
    
}

示例开发模式“载入”视图控制器 - 子类的子类着色并勾勒出 UI 元素的轮廓。它将生成上面所示的屏幕帽(在 iPad 上运行时),以便更轻松地查看正在发生的事情:OnboardingVC

// MARK: subclass of OnboardingVC
//  for use during development
//  colorizes and outlines view elements
//  centers a "simulated" device frame so we can see "outside the frame"

class DevOnboardingVC: OnboardingVC {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let g = view.safeAreaLayoutGuide
        
        // we need to re-build the outerScrollView constraints
        outerScrollView.removeFromSuperview()
        view.addSubview(outerScrollView)
        
        // we'll make the "device frame"
        //  90% of the view height, or
        //  640-points, whichever is smaller
        let targetHeightC: NSLayoutConstraint = outerScrollView.heightAnchor.constraint(equalTo: g.heightAnchor, multiplier: 0.9)
        targetHeightC.priority = .required - 1
        let maxHeightC: NSLayoutConstraint = outerScrollView.heightAnchor.constraint(lessThanOrEqualToConstant: 640.0)
        
        NSLayoutConstraint.activate([
            
            outerScrollView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            outerScrollView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            outerScrollView.widthAnchor.constraint(equalToConstant: 300.0),
            maxHeightC, targetHeightC,
            
        ])
        
        view.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
        
        outerContentView.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0)
        
        outerScrollView.clipsToBounds = false
        pageScrollView.clipsToBounds = false
        
        pageStackView.arrangedSubviews.forEach { v in
            v.backgroundColor = .white.withAlphaComponent(0.5)
            v.backgroundColor = .init(red: 1.0, green: 0.25, blue: 0.2, alpha: 1.0).withAlphaComponent(0.5)
            v.layer.borderColor = UIColor.systemBlue.cgColor
            v.layer.borderWidth = 2
        }
        self.children.forEach { vc in
            if let vc = vc as? SamplePageVC {
                vc.titleLabel.backgroundColor = .systemGreen
                vc.descLabel.backgroundColor = .systemGreen
            }
        }
        
        let dashV1 = DashedBorderView()
        dashV1.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dashV1)
        
        let dashV2 = DashedBorderView()
        dashV2.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dashV2)
        
        let dashV3 = DashedBorderView()
        dashV3.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dashV3)
        
        NSLayoutConstraint.activate([
            dashV1.topAnchor.constraint(equalTo: outerScrollView.topAnchor, constant: 0.0),
            dashV1.leadingAnchor.constraint(equalTo: outerScrollView.leadingAnchor, constant: 0.0),
            dashV1.trailingAnchor.constraint(equalTo: outerScrollView.trailingAnchor, constant: 0.0),
            dashV1.bottomAnchor.constraint(equalTo: outerScrollView.bottomAnchor, constant: 0.0),
            
            dashV2.topAnchor.constraint(equalTo: outerScrollView.topAnchor, constant: 0.0),
            dashV2.leadingAnchor.constraint(equalTo: outerScrollView.leadingAnchor, constant: 0.0),
            dashV2.trailingAnchor.constraint(equalTo: outerScrollView.trailingAnchor, constant: 0.0),
            dashV2.bottomAnchor.constraint(equalTo: outerScrollView.bottomAnchor, constant: 0.0),
            
            dashV3.topAnchor.constraint(equalTo: pageScrollView.topAnchor, constant: 0.0),
            dashV3.leadingAnchor.constraint(equalTo: pageScrollView.leadingAnchor, constant: 0.0),
            dashV3.trailingAnchor.constraint(equalTo: pageScrollView.trailingAnchor, constant: 0.0),
            dashV3.bottomAnchor.constraint(equalTo: pageScrollView.bottomAnchor, constant: 0.0),
        ])
        
        dashV1.color = .black
        dashV1.lineWidth = 16
        dashV1.position = .outside
        dashV1.dashPattern = []
        
        dashV2.color = .systemYellow
        dashV2.lineWidth = 3
        dashV2.dashPattern = [60, 8]
        
        dashV3.color = .white
        dashV3.lineWidth = 2
        
    }
    
}

虚线边框视图 - 由“Dev Mode”类使用:

class DashedBorderView: UIImageView {

    enum BorderPosition {
        case inside, middle, outside
    }
    
    public var position: BorderPosition = .middle { didSet { setNeedsLayout() } }
    public var dashPattern: [NSNumber] = [16, 16] { didSet { dashedLineLayer.lineDashPattern = dashPattern } }
    public var lineWidth: CGFloat = 1.0 { didSet { dashedLineLayer.lineWidth = lineWidth } }
    public var color: UIColor = .red { didSet { dashedLineLayer.strokeColor = color.cgColor } }
    
    override class var layerClass: AnyClass { CAShapeLayer.self }
    var dashedLineLayer: CAShapeLayer { layer as! CAShapeLayer }
    
    init() {
        super.init(frame: .zero)
        commonInit()
    }
    override init(image: UIImage?) {
        super.init(image: image)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        // this view will usually be overlaid on top of an interactive view
        //  so disable by default
        isUserInteractionEnabled = false
        
        dashedLineLayer.fillColor = UIColor.clear.cgColor
        dashedLineLayer.strokeColor = color.cgColor
        dashedLineLayer.lineWidth = lineWidth
        dashedLineLayer.lineDashPattern = dashPattern
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        switch position {
        case .inside:
            dashedLineLayer.path = CGPath(rect: bounds.insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), transform: nil)
        case .outside:
            dashedLineLayer.path = CGPath(rect: bounds.insetBy(dx: -lineWidth * 0.5, dy: -lineWidth * 0.5), transform: nil)
        case .middle:
            dashedLineLayer.path = CGPath(rect: bounds, transform: nil)
        }
    }
    
}

重要提示

  • 这只是示例代码!!它不打算,也不应被视为“生产就绪”
  • 设计是针对 iPhone 纵向的......它仍然可以在 iPad 或旋转为横向的手机上工作,但我保证它看起来不会很好:)

我还在这里发布了一个包含上述所有代码的项目:https://github.com/DonMag/PageViewApproach


编辑 - 更多解释...

使用 or(无论是使用流程、自定义还是组合布局)的最大好处是内存管理......您可以拥有 100 个“页面”,而不必担心。UIPageViewControllerUICollectionView

但是,对于这个布局目标,动态高度始终是问题所在......因为这两个类也被设计为根据我们为 PageViewController 或 CollectionView 设置的框架来“布局它们的页面/单元格”。

考虑 5 个“页面”,如下所示:

enter image description here

在页面视图控制器或集合视图中,我们无法设置帧高度(黄色矩形),因为在实例化第 4 页之前,我们不知道“所需的最大高度”。

我们可以计算负载的最大高度,例如:

func getMaxHeight() -> CGFloat {
    
    var maxHeight: CGFloat = 0.0
    for i in 0..<numPages {
        let v = MyPageView()
        v.fillLabels(forPage: i)
        let sz = v.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        maxHeight = max(maxHeight, sz.height)
    }
    return maxHeight
    
}

或者,如果我们在内存中“缓存”了页面视图:

func getMaxHeight() -> CGFloat {
    
    var maxHeight: CGFloat = 0.0
    pageViews.forEach { v in
        let sz = v.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        maxHeight = max(maxHeight, sz.height)
    }
    return maxHeight
    
}

由于您使用的是动态类型,因此我们还需要在更改时调用它并更新帧高度。UITraitCollection.preferredContentSizeCategory

就我个人而言,我会采用滚动视图方法,并简单地让自动布局为我处理它。

评论

0赞 Shawn Frank 9/17/2023
非常感谢 DonMag,惊人的答案和解释 - 正是我想要实现的。非常感谢您为此付出的时间和精力。您的假设是正确的,我们不会超过 5-6 页。作为最后一个问题,您认为UICollectionView组合布局也可以实现类似的东西吗?再次感谢,非常感谢。
0赞 DonMag 9/17/2023
@ShawnFrank - 有关其他解释,请参阅我答案底部的编辑
0赞 Shawn Frank 9/18/2023
完美,惊人的答案,涵盖了所有基础,我从你那里学到了很多东西 - 谢谢 DonMag !