UIKit rotate view 90 degrees while keeping the bounds to edge - ios

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.

Related

How to use MPVolume View correctly?

-This was my Question: This is an audio player, I removed all the other lines for you to be easy in reading. The problem is in MPVolumeView. When user swipes all the way to maximum the button of the slider hovers over the connectivity button. When user swipes the button of the slider all the way to minimum the button of the slider doesn't move to the end.
-Dear DonMag, I am really thankful to you, It works! and HOW! I am adding screenshots. I believe your answer will be helpful to a lot of self tights.
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class AudioPlayerViewControllerQ1: UIViewController {
#IBOutlet var holder: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if holder.subviews.count == 0 {
}
let volumeView = MPVolumeView(frame: CGRect(x: 20,
y: holder.frame.size.height - 80,
width: holder.frame.size.width-40,
height: 30))
holder.addSubview(volumeView)
}
private func setupView() {
setupConstraints()
}
private func setupConstraints() {
NSLayoutConstraint.activate([
holder.leadingAnchor.constraint(equalTo: view.leadingAnchor),
holder.trailingAnchor.constraint(equalTo: view.trailingAnchor),
holder.topAnchor.constraint(equalTo: view.topAnchor),
holder.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIApplication.shared.isIdleTimerDisabled = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIApplication.shared.isIdleTimerDisabled = false
}
}
After quick research and experimentation -- it appears MPVolumeView is rather buggy :(
When instantiated, if the current device volume is greater than 0, the thumb will be offset on the x-axis. The higher the volume, the larger the offset.
Also, it doesn't play well at all with auto-layout constraints.
We can get around this by subclassing MPVolumeView and "fixing" the slider rect:
class MyVolumeView: MPVolumeView {
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
// this will avoid the thumb x-offset issue
// while keeping the route button vertically aligned
return bounds.insetBy(dx: 12.0, dy: 0.0).offsetBy(dx: -12.0, dy: -5.0)
}
}
Then, to correct the problems with the vertical layout, we will want to offset the Y position when we set its frame.
Here's a quick example of one way to do that. I've embedded MyVolumeView in a "container" view, and used a property observer to update the frame whenever the container view's bounds changes:
class AudioPlayerViewControllerQ1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MyVolumeView near the bottom
let holder = UIView()
holder.backgroundColor = .darkGray
holder.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holder)
// create a separate "container" view for the MyVolumeView
let volumeViewContainer = UIView()
// we'll make it red for now so we can see it
volumeViewContainer.backgroundColor = .red
volumeViewContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(volumeViewContainer)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's make the holder 20-points inset on leading/trailing
holder.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
holder.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// holder height (for this example) is 240.0
holder.heightAnchor.constraint(equalToConstant: 240.0),
// let's put its bottom 60-points from the bottom (of the safe area)
holder.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -160.0),
// volume view container leading/trailing equal to holder
volumeViewContainer.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
volumeViewContainer.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
// volume view container bottom equal to holder bottom
volumeViewContainer.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
// volume view container height equal to 30-points
volumeViewContainer.heightAnchor.constraint(equalToConstant: 30.0),
])
// now we'll add a MPVolumeView to the container
let v = MyVolumeView()
volumeViewContainer.addSubview(v)
// we'll use a property observer to update the MyVolumeView frame
// whenever the container bounds changes
volumeViewContainer.addObserver(self, forKeyPath: "bounds", context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "bounds" {
// make sure we're getting notified of the MyVolumeView container view
if let cv = object as? UIView,
let mpv = cv.subviews.first as? MyVolumeView {
// set MyVolumeView frame to container view's bounds
// and offset its y-position by 4-points (because of its buggy layout)
mpv.frame = cv.bounds.offsetBy(dx: 0.0, dy: 4.0)
}
}
}
}
It looks like this when running:
and we can drag the thumb all the way to the left:
and to the right (without overlapping the route button):
Edit
Here are a couple simplified examples...
Using CGRect frames instead of constraints (as requested by the OP):
class AudioPlayerViewControllerQ1: UIViewController {
let holder = UIView()
let myVolumeView = MyVolumeView()
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MPVolumeView near the bottom
holder.backgroundColor = .darkGray
view.addSubview(holder)
// now we'll add a MPVolumeView to the container
holder.addSubview(myVolumeView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// let's make sure this only executes if the holder view frame has not been set yet
if holder.frame.width != 320.0 {
// set holder view frame to 320 x 240
holder.frame = CGRect(x: 0, y: 0, width: 320.0, height: 240.0)
// center it in the view
holder.center = view.center
// set myVolumeView frame to same width as holder view
// 30-points height, at bottom of holder view
myVolumeView.frame = CGRect(x: 0.0, y: holder.frame.height - 30.0, width: holder.frame.width, height: 30.0)
}
}
}
and, this one using constraints:
class AudioPlayerViewControllerQ1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's give the view a background color so we can easily see its frame
view.backgroundColor = .systemYellow
// assuming "holder" view has buttons and other controls
// along with the MPVolumeView near the bottom
let holder = UIView()
holder.backgroundColor = .darkGray
holder.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holder)
// now we'll add a MPVolumeView to the container
let myVolumeView = MyVolumeView()
myVolumeView.translatesAutoresizingMaskIntoConstraints = false
holder.addSubview(myVolumeView)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// set holder view frame to 320 x 240
holder.widthAnchor.constraint(equalToConstant: 320.0),
holder.heightAnchor.constraint(equalToConstant: 240.0),
// center it
holder.centerXAnchor.constraint(equalTo: g.centerXAnchor),
holder.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain myVolumeView leading/trailing/bottom equal to holder view
myVolumeView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0.0),
myVolumeView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0.0),
myVolumeView.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0.0),
// myVolumeView height
myVolumeView.heightAnchor.constraint(equalToConstant: 30.0),
])
}
}

How to achieve left alignment on uilabel (same as suggestions appear in native keyboard)?

What I am trying to achieve (and I am not 100% sure how to do it or how to explain it properly) is described in the below screenshot.
I have added allowsDefaultTighteningForTruncation = true and lineBreakMode = .byClipping to my label, but it now displays the beginning of the word and I need to display the end of the word, any ideas on how to achieve that? or any ideas what to look for in apple docs? I've read everything I could think of so far.
To get that result, you need to embed the label in a UIView and constrain the label's Trailing but not Leading.
Make sure the "holder" view has Clips To Bounds set to true.
As the label grows in width, it will extend past the leading edge of the holder view.
Here's a quick example:
class ViewController: UIViewController {
let theLabel = UILabel()
let holderView = UIView()
let strs: [String] = [
"Misinterpret",
"Misinterpreted",
"Misinterpretation",
]
var idx = 0
override func viewDidLoad() {
super.viewDidLoad()
theLabel.translatesAutoresizingMaskIntoConstraints = false
holderView.translatesAutoresizingMaskIntoConstraints = false
holderView.backgroundColor = .systemBlue
theLabel.backgroundColor = .yellow
theLabel.font = .systemFont(ofSize: 30.0)
holderView.addSubview(theLabel)
view.addSubview(holderView)
NSLayoutConstraint.activate([
holderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
holderView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
holderView.widthAnchor.constraint(equalToConstant: 200.0),
holderView.heightAnchor.constraint(equalTo: theLabel.heightAnchor, constant: 8.0),
theLabel.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
theLabel.trailingAnchor.constraint(equalTo: holderView.trailingAnchor, constant: -8.0),
])
// clip theLabel when it gets too wide
holderView.clipsToBounds = true
theLabel.text = strs[idx]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
idx += 1
theLabel.text = strs[idx % strs.count]
}
}
Output:
The "type ahead" suggestion bar probably also uses a gradient mask so the text does not look so abruptly clipped... but that's another question.
Edit - here's a more complete example.
textField at the top
label in a gray "holder" view
green label showing actual size of text
As you enter text, the labels will update.
The label in the gray box will be centered horizontally, until it is too wide to fit, at which point it will stay "right-aligned." It will also have a slight gradient mask at the left edge so it is not cut off abruptly.
class ViewController: UIViewController {
let textField = UITextField()
let theClippedLabel = UILabel()
let holderView = UIView()
// plain label showing the actual size
let theActualLabel = UILabel()
let leftEdgeFadeMask = CAGradientLayer()
override func viewDidLoad() {
super.viewDidLoad()
[textField, theClippedLabel, holderView, theActualLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
holderView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
theClippedLabel.backgroundColor = .clear
theActualLabel.backgroundColor = .green
textField.borderStyle = .roundedRect
textField.placeholder = "Type here..."
textField.addTarget(self, action: #selector(didEdit(_:)), for: .editingChanged)
theClippedLabel.font = .systemFont(ofSize: 30.0)
theActualLabel.font = theClippedLabel.font
holderView.addSubview(theClippedLabel)
view.addSubview(holderView)
view.addSubview(theActualLabel)
view.addSubview(textField)
// center label horizontally, unless it is wider than holderView (minus Left/Right "padding")
let cx = theClippedLabel.centerXAnchor.constraint(equalTo: holderView.centerXAnchor)
cx.priority = .defaultHigh
NSLayoutConstraint.activate([
// center a 200-pt wide "holder" view
holderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
holderView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20.0),
holderView.widthAnchor.constraint(equalToConstant: 200.0),
// holderView height is 16-pts taller than the label height (8-pts Top / Bottom "padding")
holderView.heightAnchor.constraint(equalTo: theClippedLabel.heightAnchor, constant: 16.0),
// center the label vertically
theClippedLabel.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
// keep the label's Trailing edge at least 8-pts from the holderView's Trailing edge
theClippedLabel.trailingAnchor.constraint(lessThanOrEqualTo: holderView.trailingAnchor, constant: -8.0),
// activate cx constraint
cx,
theActualLabel.topAnchor.constraint(equalTo: holderView.bottomAnchor, constant: 4.0),
theActualLabel.centerXAnchor.constraint(equalTo: holderView.centerXAnchor),
textField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12.0),
textField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12.0),
textField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12.0),
])
// clip theLabel when it gets too wide
holderView.clipsToBounds = true
// gradient mask for left-edge of label
leftEdgeFadeMask.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
leftEdgeFadeMask.startPoint = CGPoint(x: 0.0, y: 0.0)
leftEdgeFadeMask.endPoint = CGPoint(x: 1.0, y: 0.0)
leftEdgeFadeMask.locations = [0.0, 0.1]
theClippedLabel.layer.mask = leftEdgeFadeMask
// so we have something to see when we start
theClippedLabel.text = " "
theActualLabel.text = theClippedLabel.text
}
#objc func didEdit(_ sender: Any) {
// if the textField is empty, use a space character so
// the labels don't disappear
var str = " "
if let s = textField.text, !s.isEmpty {
str = s
}
theClippedLabel.text = str
theActualLabel.text = str
updateMask()
}
func updateMask() -> Void {
// update label frame
theClippedLabel.sizeToFit()
// we want the gradient mask to start at the leading edge
// of the holder view, with
// 4-pts Left and 8-pts Right "padding"
var r = holderView.bounds
let targetW = r.width - 12
r.size.width -= 12
r.size.height -= 16
r.origin.x = theClippedLabel.bounds.width - targetW
// disable built-in layer animations
CATransaction.begin()
CATransaction.setDisableActions(true)
leftEdgeFadeMask.frame = r
CATransaction.commit()
}
}
Example result:
Note that this is example code only. In practical use, we'd want to build this as a custom view with all of the sizing and gradient mask logic self-contained.

UITextView gradient layer apply not working

I want to apply gradient layer on top 10% and bottom 10% of UITextView. To do this, I place a dummy UIView called container view and make UITextView a subview of it. And then I add the following code:
if let containerView = textView.superview {
let gradient = CAGradientLayer(layer: containerView.layer)
gradient.frame = containerView.bounds
gradient.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
gradient.locations = [0.0, 0.1, 0.9, 1.0]
containerView.layer.mask = gradient
}
But the gradient is only applied to the top, not the bottom. Is there something wrong with the code?
Further, if I resize the container view anytime by modifying it's constraints, do I need to edit the mask layer every time?
Edit: Here is the output from #DonMag answer.
But what I want is something like in this image that text fades at the bottom.
EDIT2:
Here are screenshots after DonMag's revised answer.
#DongMag solution is very complicated. Instead, you just need a mask implemented like:
#IBDesignable
class MaskableLabel: UILabel {
var maskImageView = UIImageView()
#IBInspectable
var maskImage: UIImage? {
didSet {
maskImageView.image = maskImage
updateView()
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateView()
}
func updateView() {
if maskImageView.image != nil {
maskImageView.frame = bounds
mask = maskImageView
}
}
}
Then with a simple gradient mask like this, You can see it even right in the storyboard.
Note: You can use this method and replace UILabel with any other view you like to subclass.
Here is the example project on the GitHub
Edit - after clarification of desired effect...
My initial answer as to why you were only seeing the gradient on the top stands:
You're only seeing the gradient on the top because you gave it four locations but only two colors.
So, now that you provided an image of what you're trying to do...
Use this DoubleGradientMaskView as the "container" view for the text view:
class DoubleGradientMaskView: UIView {
let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0.0, 0.1, 0.9, 1.0]
layer.mask = gradientLayer
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
Example controller:
class GradientTextViewViewController: UIViewController {
let textView = UITextView()
let containerView = DoubleGradientMaskView()
let bkgImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
[bkgImageView, textView, containerView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
bkgImageView.contentMode = .scaleAspectFill
if let img = UIImage(named: "background") {
bkgImageView.image = img
} else {
bkgImageView.backgroundColor = .blue
}
view.addSubview(bkgImageView)
view.addSubview(containerView)
containerView.addSubview(textView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// add an image view so we can see the white text
bkgImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
bkgImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
bkgImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
bkgImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constraint text view inside container
textView.topAnchor.constraint(equalTo: containerView.topAnchor),
textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
// constrain container Top / Bottom 40, Leading / Trailing 40
containerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
textView.isScrollEnabled = true
textView.font = UIFont.systemFont(ofSize: 48.0, weight: .bold)
textView.textColor = .white
textView.backgroundColor = .clear
textView.text = String((1...20).flatMap { "This is row \($0)\n" })
}
}
Result:
or, with a blue background instead of an image:
You're only seeing the gradient on the top because you gave it four locations but only two colors.
Changing the colors to:
gradient.colors = [UIColor.clear.cgColor, UIColor.black.cgColor, UIColor.black.cgColor, UIColor.clear.cgColor]
would probably give you the appearance you want... but you'd need additional code to handle size changing.
If you use this class as your "container" view, sizing will be automatic:
class DoubleGradientView: UIView {
var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [UIColor.black.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor, UIColor.black.cgColor]
gradientLayer.locations = [0.0, 0.1, 0.9, 1.0]
}
}
Here is an example controller. It creates two "text views in containers."
The top one is scrollable, with a height of 100.
The bottom one is NOT scrollable, so it will size its height to the text as you type.
Both are constrained Leading / Trailing at 60-pts, so you'll also see the automatic gradient update when you rotate the device.
class GradientBehindTextViewViewController: UIViewController {
let textView1 = UITextView()
let containerView1 = DoubleGradientView()
let textView2 = UITextView()
let containerView2 = DoubleGradientView()
override func viewDidLoad() {
super.viewDidLoad()
[textView1, containerView1, textView2, containerView2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
containerView1.addSubview(textView1)
view.addSubview(containerView1)
containerView2.addSubview(textView2)
view.addSubview(containerView2)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constraint text view inside container
textView1.topAnchor.constraint(equalTo: containerView1.topAnchor),
textView1.leadingAnchor.constraint(equalTo: containerView1.leadingAnchor),
textView1.trailingAnchor.constraint(equalTo: containerView1.trailingAnchor),
textView1.bottomAnchor.constraint(equalTo: containerView1.bottomAnchor),
// constrain container Top + 40, Leading / Trailing 80
containerView1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
containerView1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 80.0),
containerView1.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -80.0),
// text view 1 will have scrolling enabled, so we'll set its height to 100
containerView1.heightAnchor.constraint(equalToConstant: 100.0),
// constraint text view inside container
textView2.topAnchor.constraint(equalTo: containerView2.topAnchor),
textView2.leadingAnchor.constraint(equalTo: containerView2.leadingAnchor),
textView2.trailingAnchor.constraint(equalTo: containerView2.trailingAnchor),
textView2.bottomAnchor.constraint(equalTo: containerView2.bottomAnchor),
// constrain container2 Top to container1 bottom + 40, Leading / Trailing 80
containerView2.topAnchor.constraint(equalTo: containerView1.bottomAnchor, constant: 40.0),
containerView2.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 80.0),
containerView2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -80.0),
// text view 2 will NOT scroll (it will size with the text) so no height / bottom
])
// text view 1 should scroll
textView1.isScrollEnabled = true
// text view 1 should NOT scroll we want the text view to size itelf as we type
textView2.isScrollEnabled = false
// let the gradient show through
textView1.backgroundColor = .clear
textView2.backgroundColor = .clear
textView1.text = "Initial text for text view 1."
textView2.text = "Initial text for text view 2."
}
}

Dynamically find the right zoom scale to fit portion of view

I have a grid view, it's like a chess board. The hierarchy is this :
UIScrollView
-- UIView
---- [UIViews]
Here's a screenshot.
Knowing that a tile has width and height of tileSide, how can I find a way to programmatically zoom in focusing on the area with the blue border? I need basically to find the right zoomScale.
What I'm doing is this :
let centralTilesTotalWidth = tileSide * 5
zoomScale = CGFloat(centralTilesTotalWidth) / CGFloat(actualGridWidth) + 1.0
where actualGridWidth is defined as tileSide multiplied by the number of columns. What I'm obtaining is to see almost seven tiles, not the five I want to see.
Keep also present that the contentView (the brown one) has a full screen frame, like the scroll view in which it's contained.
You can do this with zoom(to rect: CGRect, animated: Bool) (Apple docs).
Get the frames of the top-left and bottom-right tiles
convert then to contentView coordinates
union the two rects
call zoom(to:...)
Here is a complete example - all via code, no #IBOutlet or #IBAction connections - so just create a new view controller and assign its custom class to GridZoomViewController:
class GridZoomViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
let contentView: UIView = {
let v = UIView()
return v
}()
let gridStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.distribution = .fillEqually
return v
}()
var selectedTiles: [TileView] = [TileView]()
override func viewDidLoad() {
super.viewDidLoad()
[gridStack, contentView, scrollView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
var bColor: Bool = false
// create a 9x7 grid of tile views, alternating cyan and yellow
for _ in 1...7 {
// horizontal stack view
let rowStack = UIStackView()
rowStack.translatesAutoresizingMaskIntoConstraints = false
rowStack.axis = .horizontal
rowStack.distribution = .fillEqually
for _ in 1...9 {
// create a tile view
let v = TileView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = bColor ? .cyan : .yellow
v.origColor = v.backgroundColor!
bColor.toggle()
// add a tap gesture recognizer to each tile view
let g = UITapGestureRecognizer(target: self, action: #selector(self.tileTapped(_:)))
v.addGestureRecognizer(g)
// add it to the row stack view
rowStack.addArrangedSubview(v)
}
// add row stack view to grid stack view
gridStack.addArrangedSubview(rowStack)
}
// add subviews
contentView.addSubview(gridStack)
scrollView.addSubview(contentView)
view.addSubview(scrollView)
let padding: CGFloat = 20.0
// respect safe area
let g = view.safeAreaLayoutGuide
// for scroll view content constraints
let cg = scrollView.contentLayoutGuide
// let grid width shrink if 7:9 ratio is too tall for view
let wAnchor = gridStack.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 1.0)
wAnchor.priority = .defaultHigh
NSLayoutConstraint.activate([
// constrain scroll view to view (safe area), all 4 sides with "padding"
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: padding),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: padding),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -padding),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -padding),
// constrain content view to scroll view contentLayoutGuide, all 4 sides
contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// content view width and height equal to scroll view width and height
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0),
contentView.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor, constant: 0.0),
// activate gridStack width anchor
wAnchor,
// gridStack height = gridStack width at 7:9 ration (7 rows, 9 columns)
gridStack.heightAnchor.constraint(equalTo: gridStack.widthAnchor, multiplier: 7.0 / 9.0),
// make sure gridStack height is less than or equal to content view height
gridStack.heightAnchor.constraint(lessThanOrEqualTo: contentView.heightAnchor),
// center gridStack in contentView
gridStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0.0),
gridStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0),
])
// so we can see the frames
view.backgroundColor = .blue
scrollView.backgroundColor = .orange
contentView.backgroundColor = .brown
// delegate and min/max zoom scales
scrollView.delegate = self
scrollView.minimumZoomScale = 0.25
scrollView.maximumZoomScale = 5.0
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
if self.selectedTiles.count == 2 {
// re-zoom the content on size change (such as device rotation)
self.zoomToSelected()
}
})
}
#objc
func tileTapped(_ gesture: UITapGestureRecognizer) -> Void {
// make sure it was a Tile View that sent the tap gesture
guard let tile = gesture.view as? TileView else { return }
if selectedTiles.count == 2 {
// if we already have 2 selected tiles, reset everything
reset()
} else {
// add this tile to selectedTiles
selectedTiles.append(tile)
// if it's the first one, green background, if it's the second one, red background
tile.backgroundColor = selectedTiles.count == 1 ? UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0) : .red
// if it's the second one, zoom
if selectedTiles.count == 2 {
zoomToSelected()
}
}
}
func zoomToSelected() -> Void {
// get the stack views holding tile[0] and tile[1]
guard let sv1 = selectedTiles[0].superview,
let sv2 = selectedTiles[1].superview else {
fatalError("problem getting superviews! (this shouldn't happen)")
}
// convert tile view frames to content view coordinates
let r1 = sv1.convert(selectedTiles[0].frame, to: contentView)
let r2 = sv2.convert(selectedTiles[1].frame, to: contentView)
// union the two frames to get one larger rect
let targetRect = r1.union(r2)
// zoom to that rect
scrollView.zoom(to: targetRect, animated: true)
}
func reset() -> Void {
// reset the tile views to their original colors
selectedTiles.forEach {
$0.backgroundColor = $0.origColor
}
// clear the selected tiles array
selectedTiles.removeAll()
// zoom back to full grid
scrollView.zoom(to: scrollView.bounds, animated: true)
}
}
class TileView: UIView {
var origColor: UIColor = .white
}
It will look like this to start:
The first "tile" you tap will turn green:
When you tap a second tile, it will turn red and we'll zoom in to that rectangle:
Tapping a third time will reset to starting grid.

Is it possible to use Auto Layout in a UITextField's leftView?

I want to customize a UITextField's leftView with a view that is automatically sized depending on its contents:
func set(leftImage image: UIImage) {
let imageView = UIImageView(image: image)
let paddingContainer = UIView()
// This is the crucial point:
paddingContainer.translatesAutoresizingMaskIntoConstraints = false
paddingContainer.addSubview(imageView)
imageView.pin(toMarginsOf: paddingContainer)
leftView = paddingContainer
leftViewMode = .always
}
where the pin method just pins the image view on all four sides to the margins of the paddingContainer:
func pin(toMarginsOf view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
])
}
On iOS 12, everything works as expected, but on iOS versions < 12, the image is completely misplaced. It's not even within the bounds of the text field but in the upper left corner of my view controller's view.
To me it seems like older versions of iOS don't support using Auto Layout inside the view that you set as a text field's leftView. The documentation states:
The left overlay view is placed in the rectangle returned by the leftViewRect(forBounds:) method of the receiver.
but it doesn't state how it's placed there: By using constraints or by setting the frame directly.
Are there any reliable sources or educated guesses if using Auto Layout is supported at all for the leftView?
extension UITextField{
func setLeft(image: UIImage, withPadding padding: CGFloat = 0) {
let wrapperView = UIView.init(
frame: CGRect.init(
x: 0,
y: 0,
width: bounds.height,
height: bounds.height
)
)
let imageView = UIImageView()
imageView.image = image
imageView.contentMode = .scaleAspectFit
wrapperView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(
equalTo: wrapperView.leadingAnchor,
constant: padding
),
imageView.trailingAnchor.constraint(
equalTo: wrapperView.trailingAnchor,
constant: -padding
),
imageView.topAnchor.constraint(
equalTo: wrapperView.topAnchor,
constant: padding
),
imageView.bottomAnchor.constraint(
equalTo: wrapperView.bottomAnchor,
constant: -padding
)
])
leftView = wrapperView
leftViewMode = .always
}
}
hope this will help
You can set its frame on layout subviews function like this
override func layoutSubviews() {
super.layoutSubviews()
if let lv = self.leftView {
lv.frame = CGRect(x: 0, y: 0, width: self.bounds.height, height: self.bounds.height)
}
}

Resources