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").
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. The label is constrained inside the "container" view, with 20-pts "padding" on all 4 sides. The "container" view is constrained with 12-pts "padding" on all 4 sides.
It looks like this to start:

Changing the label text to "This string will likely need to wrap onto two lines." and it looks like this:

So far, pretty basic.
To get a UIImage
of it, we can add this property:
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 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.
Each time we tap the "On Screen" button, the text in the custom view's label will cycle through 4 sample strings:


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


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.