Update:
I have since tried setting my layer anchor paint to (0,0) and translate it back to frame (0,0) after rotation using this tutorial.
This, however, still doesn't address the early wrapping issue. See below. Setting the content inset on the right side (bottom side) does not work.
textView.frame = CGRect(x: 0, y: 0, width: view.bounds.height, height: view.bounds.width)
print(textView.frame)
textView.setAnchorPoint(CGPoint(x: 0, y: 0))
textView.transform = CGAffineTransform(rotationAngle: (CGFloat)(Double.pi/2));
print(textView.frame)
textView.transform = textView.transform.translatedBy(x: 0, y: -(view.bounds.width))
print(textView.frame)
textView.contentInset = UIEdgeInsets(top: 0, left: height, bottom: 0, right: 0)
Original question: I want to rotate the only UIView in subview clockwise by 90 degrees while keeping its bounds to edges of the screen, that is, below the Navigation Bar and above the Tab Bar and in between two sides.
Normally there are two ways to do this, either set translatesAutoresizingMaskIntoConstraints to true and set subview.frame = view.bounds
or set translatesAutoresizingMaskIntoConstraints to false and add four anchors constraints (top, bottom,leading, trailing) to view's four anchor constrains.
This is what it usually will do.
However, if I were to rotate the view while keeping its bound like before, like below, how would I do that?
Here's my current code to rotate a UITextView 90 degrees clockwise. I don't have a tab bar in this case but it shouldn't matter. Since before the textview grows towards the bottom, after rotation the textview should grow towards the left side. The problem is, it's not bound to the edge I showed, it is behind the nav bar.
var textView = UITextView()
view.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = true
textView.transform = CGAffineTransform(rotationAngle: (CGFloat)(Double.pi/2));
textView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
Here it completely disappears if I rotate it after setting the frames
I also tried adding arbitrary value to the textView's y frame like so
textView.frame = CGRect(x: 0, y: 100, width: view.bounds.width, height: view.bounds.height)
but the result is that words get wrapped before they reach the bottom edge.
I also tried adding constrains by anchor and setting translateautoresizingmaskintoconstraints to false
constraints.append(contentsOf: [
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
but the result is still a white screen.
Besides what I showed, I experimented with a lot of things, adding bit of value here and there, but they all are kind of a hack and doesn't really scale. For example, if the device gets rotated to landscape mode, the entire view gets screwed up again.
So, my question is, what is the correct, scalable way to do this? I want to have this rotated textview that is able to grow(scroll) towards the left and correctly resized on any device height and any orientation.
I know this could be related to anchor point. But since I want my view to actually bound to edges and not just display its content like an usual rotated UIImage. What are the steps to achieve that?
Thank you.
We need to embed the UITextView
in a UIView
"container" ... constraining it to that container ... and then rotate the container view.
Because the textView
will continue to have a "normal" rotation relative to its superview, it will behave as desired.
Quick example:
class ViewController: UIViewController {
let textView = UITextView()
let containerView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
textView.translatesAutoresizingMaskIntoConstraints = false
// we're going to explicitly set the container view's frame
containerView.translatesAutoresizingMaskIntoConstraints = true
containerView.addSubview(textView)
view.addSubview(containerView)
// we'll inset the textView by 8-points on all sides
// so we can see that it's inside the container view
// avoid auto-layout error/warning messages
let cTrailing = textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0)
cTrailing.priority = .required - 1
let cBottom = textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0)
cBottom.priority = .required - 1
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
// activate trailing and bottom constraints
cTrailing, cBottom,
])
textView.font = .systemFont(ofSize: 32.0, weight: .regular)
textView.text = "The quick red fox jumps over the lazy brown dog, and then goes to the kitchen to get some dinner."
// so we can see the framing
textView.backgroundColor = .yellow
containerView.backgroundColor = .systemBlue
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// inset the container view frame by 40
// leaving some empty space around it
// so we can tap the view
containerView.frame = view.frame.insetBy(dx: 40.0, dy: 40.0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// end editing if textView is being edited
view.endEditing(true)
// if container view is NOT rotated
if containerView.transform == .identity {
// rotate it
containerView.transform = CGAffineTransform(rotationAngle: (CGFloat)(Double.pi/2));
} else {
// set it back to NOT rotated
containerView.transform = .identity
}
}
}
Output:
tapping anywhere white (so, tapping the controller's view instead of the container or the textView itself) will toggle rotated/non-rotated:
Edit - responding to comment...
The reason we have to work with the frame
is because of the way .transform
works.
When we apply a .transform
it changes the frame of the view, but not its bounds.
Take a look at this quick example:
class ExampleViewController: UIViewController {
let greenLabel = UILabel()
let yellowLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
greenLabel.translatesAutoresizingMaskIntoConstraints = false
yellowLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(greenLabel)
view.addSubview(yellowLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
yellowLabel.widthAnchor.constraint(equalToConstant: 200.0),
yellowLabel.heightAnchor.constraint(equalToConstant: 80.0),
yellowLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
yellowLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
greenLabel.topAnchor.constraint(equalTo: yellowLabel.topAnchor),
greenLabel.leadingAnchor.constraint(equalTo: yellowLabel.leadingAnchor),
greenLabel.trailingAnchor.constraint(equalTo: yellowLabel.trailingAnchor),
greenLabel.bottomAnchor.constraint(equalTo: yellowLabel.bottomAnchor),
])
yellowLabel.text = "Yellow"
greenLabel.text = "Green"
yellowLabel.textAlignment = .center
greenLabel.textAlignment = .center
yellowLabel.backgroundColor = .yellow.withAlphaComponent(0.80)
greenLabel.backgroundColor = .green
// we'll give the green label a larger, red font
greenLabel.font = .systemFont(ofSize: 48.0, weight: .bold)
greenLabel.textColor = .systemRed
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// if yellow label is NOT rotated
if yellowLabel.transform == .identity {
// rotate it
yellowLabel.transform = CGAffineTransform(rotationAngle: (CGFloat)(Double.pi/2));
} else {
// set it back to NOT rotated
yellowLabel.transform = .identity
}
print("Green - frame: \(greenLabel.frame) bounds: \(greenLabel.bounds)")
print("Yellow - frame: \(yellowLabel.frame) bounds: \(yellowLabel.bounds)")
}
}
We've created two labels, with the yellow label on top of the green label, and the green label constrained top/leading/trailing/bottom to the yellow label.
Notice that when we apply a rotation transform to the yellow label, the green label does not change.
If you look at the debug console output, you'll see that the yellow label's frame changes, but its bounds stays the same:
// not rotated
Green - frame: (87.5, 303.5, 200.0, 80.0) bounds: (0.0, 0.0, 200.0, 80.0)
Yellow - frame: (87.5, 303.5, 200.0, 80.0) bounds: (0.0, 0.0, 200.0, 80.0)
// rotated
Green - frame: (87.5, 303.5, 200.0, 80.0) bounds: (0.0, 0.0, 200.0, 80.0)
Yellow - frame: (147.5, 243.5, 80.0, 200.0) bounds: (0.0, 0.0, 200.0, 80.0)
So... to make use of auto-layout / constraints, we want to create a custom UIView
subclass, which has the "container" view and the text view. Something like this:
class RotatableTextView: UIView {
public var containerInset: CGFloat = 0.0 { didSet { setNeedsLayout() } }
public var textViewInset: CGFloat = 0.0 {
didSet {
tvConstraints[0].constant = textViewInset
tvConstraints[1].constant = textViewInset
tvConstraints[2].constant = -textViewInset
tvConstraints[3].constant = -textViewInset
}
}
public let textView = UITextView()
public let containerView = UIView()
private var tvConstraints: [NSLayoutConstraint] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
textView.translatesAutoresizingMaskIntoConstraints = false
// we're going to explicitly set the container view's frame
containerView.translatesAutoresizingMaskIntoConstraints = true
containerView.addSubview(textView)
addSubview(containerView)
// avoid auto-layout error/warning messages
var c: NSLayoutConstraint!
c = textView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: textViewInset)
tvConstraints.append(c)
c = textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: textViewInset)
tvConstraints.append(c)
c = textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -textViewInset)
c.priority = .required - 1
tvConstraints.append(c)
c = textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -textViewInset)
c.priority = .required - 1
tvConstraints.append(c)
NSLayoutConstraint.activate(tvConstraints)
}
func toggleRotation() {
// if container view is NOT rotated
if containerView.transform == .identity {
// rotate it
containerView.transform = CGAffineTransform(rotationAngle: (CGFloat)(Double.pi/2));
} else {
// set it back to NOT rotated
containerView.transform = .identity
}
}
override func layoutSubviews() {
super.layoutSubviews()
var r = CGRect(origin: .zero, size: frame.size)
containerView.frame = r.insetBy(dx: containerInset, dy: containerInset)
}
}
Now, in our controller, we can use constraints on our custom view like we always do:
class ViewController: UIViewController {
let rotTextView = RotatableTextView()
override func viewDidLoad() {
super.viewDidLoad()
rotTextView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(rotTextView)
// we'll inset the custom view on all sides
// so we can tap on the "root" view to toggle the rotation
let inset: CGFloat = 20.0
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
rotTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: inset),
rotTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: inset),
rotTextView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -inset),
rotTextView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -inset),
])
// let's use a 32-point font
rotTextView.textView.font = .systemFont(ofSize: 32.0, weight: .regular)
// give it some initial text
rotTextView.textView.text = "The quick red fox jumps over the lazy brown dog, and then goes to the kitchen to get some dinner."
// if we want to inset either the container or the text view
//rotTextView.containerInset = 8.0
//rotTextView.textViewInset = 4.0
// so we can see the framing if insets are > 0
// if both insets are 0, we won't see these, so they don't need to be set
//rotTextView.backgroundColor = .systemBlue
//rotTextView.containerView.backgroundColor = .systemYellow
// let's set the text view background color to light-cyan
// so we can see its frame
rotTextView.textView.backgroundColor = UIColor(red: 0.75, green: 1.0, blue: 1.0, alpha: 1.0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// end editing if textView is being edited
view.endEditing(true)
rotTextView.toggleRotation()
}
}
Note that this is just Example Code, but it should get you on your way.