简体   繁体   English

如何在不将视图添加到视图层次结构的情况下触发屏幕外自动布局

[英]How to trigger auto layout offscreen without adding the view to view hierarchy

My scenario is that I need to generate PNG Image data from an UIView whose layout I will setup myself.我的场景是我需要从 UIView 生成 PNG 图像数据,我将自行设置其布局。 I create an UIView viewA in memory, which I will use to create PNG data.我在 memory 中创建了一个 UIView viewA,我将使用它来创建 PNG 数据。 This happens in a framework I'm working on, meaning that my viewA can only exist in memory and will never get attached to view hierarchy/go on screen.这发生在我正在处理的框架中,这意味着我的 viewA 只能存在于 memory 中,并且永远不会附加到视图层次结构/进入屏幕。

However, based on my current testing, autolayout can only be effective when the view is attached to view hierarchy so that the system can trigger layout pass and related events such as layoutSubview and updateConstraint.但是,根据我目前的测试,自动布局只有在视图附加到视图层次结构时才有效,以便系统可以触发布局传递和相关事件,例如 layoutSubview 和 updateConstraint。 Without being on screen, calls like setNeedsLayout and layoutSubview are ineffective because no layout pass will be run at all.如果不在屏幕上,像 setNeedsLayout 和 layoutSubview 这样的调用是无效的,因为根本不会运行任何布局传递。

I can still manually setup frame of ViewA, but just wondering is there a way to trigger autolayout even when the view stays offscreen.我仍然可以手动设置 ViewA 的框架,但只是想知道即使视图不在屏幕上也能触发自动布局。

It would help if you provided a more concrete description of what you want to do with your "offscreen view."如果您提供更具体的描述来说明您想用“屏幕外视图”做什么,将会有所帮助。

However, here's an example of getting a UIImage from a UIView that has never been added to the view hierarchy (so it is "offscreen").但是,这里有一个从从未添加到视图层次结构中的UIView获取UIImage的示例(因此它是“离屏”)。

First, the simple view subclass:一、简单的视图子类:

class MySimpleView: UIView {

    let label: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.numberOfLines = 0
        v.backgroundColor = .yellow
        v.text = "Multiline Label"
        return v
    }()
    let containerView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemBlue
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        [label, containerView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        containerView.addSubview(label)
        self.addSubview(containerView)
        NSLayoutConstraint.activate([
            
            containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 12.0),
            containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12.0),
            containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12.0),
            containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12.0),
            
            label.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
            label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
            label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
            label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),

            // label max width: 240
            label.widthAnchor.constraint(lessThanOrEqualToConstant: 240.0),
            
        ])
        
        self.backgroundColor = .systemRed
    }
    
}

It has a label - .numberOfLines = 0 and max-width of 240 - as a subview of a "container" view, which is a subview of itself.它有一个 label - .numberOfLines = 0240的最大宽度 - 作为“容器”视图的子视图,它是自身的子视图。 The label is constrained inside the "container" view, with 20-pts "padding" on all 4 sides. label 被限制在“容器”视图内,所有 4 个侧面都有 20 点“填充”。 The "container" view is constrained with 12-pts "padding" on all 4 sides. “容器”视图在所有 4 面都受到 12 点“填充”的约束。

It looks like this to start:它看起来像这样开始:

在此处输入图像描述

Changing the label text to "This string will likely need to wrap onto two lines."将 label 文本更改为“此字符串可能需要换行成两行”。 and it looks like this:它看起来像这样:

在此处输入图像描述

So far, pretty basic.到目前为止,非常基本。

To get a UIImage of it, we can add this property:要获得它的UIImage ,我们可以添加以下属性:

var image: UIImage {
    get {
        self.setNeedsLayout()
        self.layoutIfNeeded()
        let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
        return renderer.image { _ in
            self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        }
    }
    set {}
}

By including:通过包括:

self.setNeedsLayout()
self.layoutIfNeeded()

we can get the view to update itself, even if it's not in the view hierarchy.我们可以让视图自行更新,即使它不在视图层次结构中。

Here's the completed class, along with an example controller:这是完整的 class 以及示例 controller:

class MySimpleView: UIView {

    var image: UIImage {
        get {
            self.setNeedsLayout()
            self.layoutIfNeeded()
            let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
            return renderer.image { _ in
                self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
            }
        }
        set {}
    }

    let label: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.numberOfLines = 0
        v.backgroundColor = .yellow
        v.text = "Multiline Label"
        return v
    }()
    let containerView: UIView = {
        let v = UIView()
        v.backgroundColor = .systemBlue
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        [label, containerView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        containerView.addSubview(label)
        self.addSubview(containerView)
        NSLayoutConstraint.activate([
            
            containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 12.0),
            containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12.0),
            containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12.0),
            containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12.0),
            
            label.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
            label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
            label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
            label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),

            // label max width: 240
            label.widthAnchor.constraint(lessThanOrEqualToConstant: 240.0),
            
        ])
        
        self.backgroundColor = .systemRed
    }
    
}

class OffScreenTestViewController: UIViewController {

    let onScreentestStrings: [String] = [
        "Short String",
        "A bit longer String",
        "This string will likely need to wrap onto two lines.",
        "This string is going to be really, really long, and will almost certainly need to wrap onto more than two lines.",
    ]
    
    let offScreentestStrings: [String] = [
        "Off-screen String",
        "A bit longer Off-screen String",
        "Off-screen string will likely need to wrap onto two lines.",
        "This Off-screen string is going to be really, really long, and will almost certainly need to wrap onto more than two lines.",
    ]
    
    var onScreenIDX: Int = 0
    var offScreenIDX: Int = 0

    let onScreenTestView = MySimpleView()
    let offScreenTestView = MySimpleView()

    let resultsImageView: UIImageView = {
        let v = UIImageView()
        v.contentMode = .scaleAspectFit
        v.backgroundColor = .systemGreen
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        onScreenTestView.translatesAutoresizingMaskIntoConstraints = false
        resultsImageView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(onScreenTestView)
        view.addSubview(resultsImageView)
        
        let stack: UIStackView = {
            let v = UIStackView()
            v.spacing = 20
            v.distribution = .fillEqually
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        ["On Screen", "Off Screen"].forEach { title in
            let b = UIButton()
            b.backgroundColor = .systemBlue
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.setTitle(title, for: [])
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
            stack.addArrangedSubview(b)
        }
        view.addSubview(stack)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

            onScreenTestView.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
            onScreenTestView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            resultsImageView.topAnchor.constraint(equalTo: onScreenTestView.bottomAnchor, constant: 20.0),
            resultsImageView.widthAnchor.constraint(equalToConstant: 240.0),
            resultsImageView.heightAnchor.constraint(equalTo: resultsImageView.widthAnchor),
            resultsImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
        ])
        
        // needs this, even though we're not adding it to the view hierarchy
        offScreenTestView.translatesAutoresizingMaskIntoConstraints = false
        
        // just to make it really, really clear that the
        //  off-screen view is being used to generate the image
        offScreenTestView.label.backgroundColor = .blue
        offScreenTestView.label.textColor = .yellow
        if let font = UIFont(name: "SnellRoundhand-Black", size: 22.0) {
            offScreenTestView.label.font = font
        }
        offScreenTestView.containerView.backgroundColor = .systemYellow
        offScreenTestView.backgroundColor = .systemOrange
        
    }

    @objc func btnTapped(_ btn: UIButton) {
        
        guard let t = btn.currentTitle else { return }
        
        if t == "On Screen" {
            onScreenTestView.label.text = onScreentestStrings[onScreenIDX % onScreentestStrings.count]
            onScreenIDX += 1
        } else {
            offScreenTestView.label.text = offScreentestStrings[offScreenIDX % offScreentestStrings.count]
            let img = offScreenTestView.image
            resultsImageView.image = img
            offScreenIDX += 1
        }
        
    }
    
}

When you run this, it will start out looking like this:当你运行它时,它会开始看起来像这样:

在此处输入图像描述

The green square is a UIImageView set to .scaleAspectFit with no image to begin with.绿色方块是一个UIImageView设置为.scaleAspectFit没有图像开始。

Each time we tap the "On Screen" button, the text in the custom view's label will cycle through 4 sample strings:每次我们点击“On Screen”按钮时,自定义视图的 label 中的文本将循环显示 4 个示例字符串:

在此处输入图像描述 在此处输入图像描述

在此处输入图像描述 在此处输入图像描述

We've also created an instance of MySimpleView called offScreenTestView and changed some of its properties... label font and subview colors, just to make it abundantly clear it's not the same instance.我们还创建了一个名为offScreenTestViewMySimpleView实例并更改了它的一些属性... label 字体和子视图 colors,只是为了清楚地表明它不是同一个实例。

Each tap on the "Off Screen" button will cycle through a similar set of strings for the label and set the green image view's .image to offScreenTestView.image :每次点击“Off Screen”按钮都会在 label 的一组类似字符串中循环,并将绿色图像视图的.image设置为offScreenTestView.image

在此处输入图像描述 在此处输入图像描述

在此处输入图像描述 在此处输入图像描述

All of the green image view updates are happening while offScreenTestView - which is using constraints for its own sizing - has never been added to the view hierarchy.所有绿色图像视图更新都发生在offScreenTestView ——它使用约束来调整自己的大小——从未添加到视图层次结构中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM