Autolayout without intrinsic size - uiview

Can anyone tell me how Autolayout knows the size of a UIView which does not have an intrinsic size and only one dimension has been constrained?
For example, I have a plain view containing a set of subviews. The view is constrained to the leading and trailing margins of the parent and centered vertically. Autolayout is able to ask the view for its height for a given width but I don't know how. I need to layout the subviews by hand but I cannot see which function/property I need to implement so Autolayout can tell me the width and ask for the corresponding height.
Any one know?

As a general rule, the more complex the layout, the more reason to use auto-layout.
However, if you really want to "hand layout" your subviews and calculate the intrinsic size, you can do it by overriding intrinsicContentSize.
So, for example, if you calculate your subviews sizes / positions in layoutSubviews(), you should also calculate the height there, call invalidateIntrinsicContentSize(), and return your calculated height.
Here's a quick example. We add two subviews, one above the other. We give the top view a 4:1 ratio and the bottom view a 3:1 ratio.
class MyIntrinsicView: UIView {
var myHeight: CGFloat = 0
let view1: UIView = {
let v = UIView()
v.backgroundColor = .red
return v
}()
let view2: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(view1)
addSubview(view2)
backgroundColor = .blue
}
override var intrinsicContentSize: CGSize {
var sz = super.intrinsicContentSize
sz.height = myHeight
return sz
}
override func layoutSubviews() {
super.layoutSubviews()
// let's say view 1 needs a 4:1 ratio and
// view 2 needs a 3:1 ratio
view1.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.width / 4.0)
view2.frame = CGRect(x: 0, y: view1.frame.maxY, width: bounds.width, height: bounds.width / 3.0)
myHeight = view1.frame.height + view2.frame.height
invalidateIntrinsicContentSize()
}
}
Using this example view controller, where we give the view 60-pts leading and trailing, and only centerY constraints:
class QuickExampleViewController: UIViewController {
let testView = MyIntrinsicView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
We get this output:

Related

UIKit UIStackView with auto width

I'm making an app with UIkit and I'm making some UITableViewCells that contain an array of user images. I want these images to be displayed in a horizontal stack and overlay each other.
This is how I want it to look:
That's how it looks:
Code:
import UIKit
import SDWebImage
class CountryTableCell: UITableViewCell {
static let reuseIdentifier = "CountryTableCell"
//MARK: - Propeties
var viewModel: CountryViewModel? {
didSet {
viewModel?.delegate = self
setUpSpace(viewModel)
}
}
//MARK: - SubViews
private let container: UIView = {
let view = UIView()
view.withSize(CGSize(width: Dimensions.maxSafeWidth, height: 150))
view.backgroundColor = .secondary_background
view.layer.cornerRadius = 20
return view
}()
private let adminsContainer: UIView = {
let view = UIView()
view.withSize(CGSize(width: (Dimensions.maxSafeWidth - 32), height: Dimensions.image.mediumHeigthWithOverlay))
view.backgroundColor = .secondary_background
return view
}()
private let adminsStack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .leading
stack.spacing = -10
stack.sizeToFit()
stack.distribution = .fillEqually
stack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
return stack
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: UIFont.preferredFont(forTextStyle: .title2).pointSize, weight: .bold)
label.textColor = .primary_label
label.textAlignment = .left
label.sizeToFit()
label.numberOfLines = 0
return label
}()
var headerView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.withSize(CGSize(width: Dimensions.image.headerSizeForCell.width, height: ((Dimensions.image.mediumHeigth/2) + Padding.horizontal)))
iv.backgroundColor = .secondary_background
iv.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
iv.layer.cornerRadius = 20
return iv
}()
//MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
addSubview(container)
container.center(inView: self)
addSubview(headerView)
headerView.anchor(top: container.topAnchor, left: container.leftAnchor, right: container.rightAnchor)
addSubview(nameLabel)
nameLabel.anchor(top: headerView.bottomAnchor, left: container.leftAnchor, right: container.rightAnchor, paddingLeft: Padding.horizontal, paddingRight: Padding.horizontal)
addSubview(adminsContainer)
adminsContainer.anchor(left: container.leftAnchor, bottom: headerView.bottomAnchor, right: container.rightAnchor ,paddingLeft: Padding.horizontal, paddingBottom: -(Dimensions.image.mediumHeigth/2), paddingRight: Padding.horizontal)
bringSubviewToFront(adminsContainer)
adminsContainer.backgroundColor = .clear.withAlphaComponent(0.0)
adminsContainer.addSubview(adminsStack)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Lifecycle
override func willRemoveSubview(_ subview: UIView) {
}
//MARK: Selectors
//MARK: - Helpers
private func setUpSpace(_ viewModel: CountryViewModel?) {
guard let viewModel = viewModel else {return}
if let url = viewModel.country.headerImage?.url {
headerView.sd_setImage(with: URL(string: url))
}
nameLabel.text = viewModel.space.name
}
}
// MARK: Extension
extension SpaceTableCell: CountryViewModelDelegate {
func didFetchAdmin(_ admin: User) {
if let admins = viewModel?.admins.count {
if admins == 1 {
adminsStack.withSize(CGSize(width: Dimensions.image.mediumHeigthWithOverlay, height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 2 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*2) - 10), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 3 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*3) - 20), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 4 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*4) - 30), height: Dimensions.image.mediumHeigthWithOverlay))
} else if admins == 5 {
adminsStack.withSize(CGSize(width: ((Dimensions.image.mediumHeigthWithOverlay*5) - 40), height: Dimensions.image.mediumHeigthWithOverlay))
}
}
let image = UserImageView(height: Dimensions.image.mediumHeigth)
image.sd_setImage(with: admin.profileImageURL)
adminsStack.addArrangedSubview(image)
}
}
class UserImageView: UIImageView {
//MARK: - Propeties
let selectedHeigth: CGFloat
init(height: CGFloat) {
self.selectedHeigth = height
super.init(frame: CGRect(x: 0, y: 0, width: selectedHeigth, height: selectedHeigth))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
contentMode = .scaleAspectFill
clipsToBounds = true
backgroundColor = .secondary_background
layer.cornerRadius = Dimensions.userImageCornerRadious(selectedHeigth)
}
}
Can someone please help me
Thank you :)
I'd suggest a couple changes:
Change #1:
Your UIStackView's distribution set to .fillEqually. By doing this, you're giving it the permission to stretch its arrangedSubviews as and when needed to fit the width that you've provided. This does prove useful in some cases but in your case, it's better to go with .fill instead
Here's an excerpt from Apple's documentation:
For all distributions except the UIStackView.Distribution.fillEqually
distribution, the stack view uses each arranged view’s
intrinsicContentSize property when calculating its size along the
stack’s axis. UIStackView.Distribution.fillEqually resizes all the
arranged views so they’re the same size, filling the stack view along
its axis. If possible, the stack view stretches all the arranged views
to match the view with the longest intrinsic size along the stack’s
axis.
I suggest that you go through this documentation and read more about this
Change #2:
Since you've now changed the distribution to respect intrinsic size, don't set up constraints that conflict with UIStackView's distribution axis. In your case, you have a horizontal stack view and seem to be also defining its width constraints based on the number of admins you have. I'd suggest getting rid of it and leave it to your stack view to take care of it.
One more pointer:
Make sure your UserImageViews have proper unified intrinsicContentSize i.e: the images you set follow the same size. If doubtful, I'd suggest setting up constraints for its width and height respectively. To do so, in your UserImageView's init, include:
self.widthAnchor.constraint(equalToConstant: height).isActive = true
self.heightAnchor.constraint(equalToConstant: height).isActive = true

UIKit rotate view 90 degrees while keeping the bounds to edge

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.

How to animate a UILabel that resizes in parallel with its container view

I am trying to animate a multi-line label inside a UIView. In the container view, the width of the label is relative to the bounds. When the container view is animated, the label jumps to the final state and then the container resizes. How can I instead animate the right side of the text to be continuously pinned to the right edge of the container view as it grows larger?
class ViewController: UIViewController {
var container: ContainerView = ContainerView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
container.center = view.center
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
}
}
}
class ContainerView: UIView {
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.text = "foo bar foo bar foo bar foo bar foo bar foo bar foo foo bar foo bar foo bar foo bar foo bar foo bar foo"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .purple
addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
let size = label.sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
label.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: size.height)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As you've seen, when we change the width of a label UIKit re-calculates the word wrapping immediately.
When we do something like this:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
UIKit sets the width and then animates it. So, as soon as the animation starts, the word wrapping gets set to the "destination" width.
One way to animate the word wrap changes would be to create an animation loop, using small point-size changes.
That works-ish, with two problems:
Using a UILabel, we get vertical shifting (because the text is vertically centered in a label), and
If we make the incremental size changes small, it's smooth but slow. If we make the incremental changes large, it's quick but "jerky."
To solve the first problem, we can use a UITextView, subclassed to work like a top-aligned UILabel. Here's an example:
class MyTextViewLabel: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
Not much we can do about the second problem, other than experiment with the width-increment value.
Here's a complete example to look at and play with (using the above MyTextViewLabel class). Note that I'm also using auto-layout / constraints instead of explicit frames:
class MyContainerView: UIView {
let label: MyTextViewLabel = {
let label = MyTextViewLabel()
label.text = "Let's use some readable text for this example. It will make the wrapping changes look more natural than using a bunch of repeating three-character \"words.\""
// let's set the font to the default UILabel font
label.font = .systemFont(ofSize: 17.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
backgroundColor = .purple
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// let's inset the "label" by 4-points so we can see the purple view frame
label.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4.0),
// if we want the bottom text to be "clipped"
// don't set the bottom anchor
//label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4.0),
])
label.backgroundColor = .yellow
}
}
class LabelWrapAnimVC: UIViewController {
// for this example
let startWidth: CGFloat = 150.0
let targetWidth: CGFloat = 200.0
// number of points to increment in each loop
// play with this value...
// 1-point produces a very smooth result, but the total animation time will be slow
// 5-points seems "reasonable" (looks smoother on device than on simulator)
let loopIncrement: CGFloat = 5.0
// total amount of time for the animation
let loopTotalDuration: TimeInterval = 2.0
// each loop anim duration - will be calculated
var loopDuration: TimeInterval = 0
let container: MyContainerView = MyContainerView()
var cWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
cWidth = container.widthAnchor.constraint(equalToConstant: startWidth)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
container.centerYAnchor.constraint(equalTo: g.centerYAnchor),
container.heightAnchor.constraint(equalTo: container.widthAnchor),
cWidth,
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
doAnim()
}
func animLoop() {
cWidth.constant += loopIncrement
// in case we go over the target width
cWidth.constant = min(cWidth.constant, targetWidth)
UIView.animate(withDuration: loopDuration, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
if self.cWidth.constant < self.targetWidth {
self.animLoop()
} else {
// maybe do something when animation is done
}
})
}
func doAnim() {
// reset width to original
cWidth.constant = startWidth
// calculate loop duration based on size difference
let numPoints: CGFloat = targetWidth - startWidth
let numLoops: CGFloat = numPoints / loopIncrement
loopDuration = loopTotalDuration / numLoops
DispatchQueue.main.async {
self.animLoop()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
doAnim()
}
}
I don't know if this will be suitable for your target usage, but it's at least worth a look.

inputAccessoryView, API error? _UIKBCompatInputView? UIViewNoIntrinsicMetric, simple code, can't figure out

Help me in one of the two ways maybe:
How to solve the problem? or
How to understand the error message?
Project summary
So I'm learning about inputAccessoryView by making a tiny project, which has only one UIButton. Tapping the button summons the keyboard with inputAccessoryView which contains 1 UITextField and 1 UIButton. The UITextField in the inputAccessoryView will be the final firstResponder that is responsible for the keyboard with that inputAccessoryView
The error message
API error: <_UIKBCompatInputView: 0x7fcefb418290; frame = (0 0; 0 0); layer = <CALayer: 0x60000295a5e0>> returned 0 width, assuming UIViewNoIntrinsicMetric
The code
is very straightforward as below
The custom UIView is used as inputAccessoryView. It installs 2 UI outlets, and tell responder chain that it canBecomeFirstResponder.
class CustomTextFieldView: UIView {
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
let textField:UITextField = {
let textField = UITextField()
textField.placeholder = "placeholder"
return textField
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
initSetup()
}
override init(frame:CGRect) {
super.init(frame: frame)
initSetup()
}
convenience init() {
self.init(frame: .zero)
}
func initSetup() {
addSubview(doneButton)
addSubview(textField)
}
func autosizing(to vc: UIViewController) {
frame = CGRect(x: 0, y: 0, width: vc.view.frame.size.width, height: 40)
let totalWidth = frame.size.width - 40
doneButton.frame = CGRect(x: totalWidth * 4 / 5 + 20,
y: 0,
width: totalWidth / 5,
height: frame.size.height)
textField.frame = CGRect(x: 20,
y: 0,
width: totalWidth * 4 / 5,
height: frame.size.height)
}
override var canBecomeFirstResponder: Bool { true }
override var intrinsicContentSize: CGSize {
CGSize(width: 400, height: 40)
} // overriding this variable seems to have no effect.
}
Main VC uses the custom UIView as inputAccessoryView. The UITextField in the inputAccessoryView becomes the real firstResponder in the end, I believe.
class ViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customView.autosizing(to: self)
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I've seen people on the internet says this error message will go away if I run on a physical phone. I didn't go away when I tried.
I override intrinsicContentSize of the custom view, but it has no effect.
The error message shows twice together when I tap summon.
What "frame" or "layer" does the error message refer to? Does it refer to the custom view's frame and layer?
If we use Debug View Hierarchy we can see that _UIKBCompatInputView is part of the (internal) view hierarchy of the keyboard.
It's not unusual to see constraint errors / warnings with internal views.
Since frame and/or intrinsic content size seem to have no effect, I don't think it can be avoided (nor does it seem to need to be).
As a side note, you can keep the "Done" button round by using auto-layout constraints. Here's an example:
class CustomTextFieldView: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 16)
tf.autocorrectionType = .no
tf.returnKeyType = .done
tf.placeholder = "placeholder"
// textField backgroundColor so we can see its frame
tf.backgroundColor = .yellow
return tf
}()
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
[doneButton, textField].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
// constrain doneButton
// Trailing: 20-pts from trailing
doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// Top and Bottom 8-pts from top and bottom
doneButton.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
doneButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// Width equal to default height
// this will keep the button round instead of oval
doneButton.widthAnchor.constraint(equalTo: doneButton.heightAnchor),
// constrain textField
// Leading: 20-pts from leading
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// Trailing: 8-pts from doneButton leading
textField.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -8.0),
// vertically centered
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class CustomTextFieldViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}

UICollectionViewCell layout with SnapKit

I have a simple UICollectionViewCell which is full width and I'm using SnapKit to layout its subviews. I'm using SnapKit for my other view and it's working great but not within the collectionViewCell. This is the layout I'm trying to achieve:
collectionViewCell layout
The code for the collectionViewCell is:
lazy var imageView: UIImageView = {
let img = UIImageView(frame: .zero)
img.contentMode = UIViewContentMode.scaleAspectFit
img.backgroundColor = UIColor.lightGray
return img
}()
override init(frame: CGRect) {
super.init(frame: frame)
let imageWidth = CGFloat(frame.width * 0.80)
let imageHeight = CGFloat(imageWidth/1.77)
backgroundColor = UIColor.darkGray
imageView.frame.size.width = imageWidth
imageView.frame.size.height = imageHeight
contentView.addSubview(imageView)
imageView.snp.makeConstraints { (make) in
make.top.equalTo(20)
//make.right.equalTo(-20)
//make.right.equalTo(contentView).offset(-20)
//make.right.equalTo(contentView.snp.right).offset(-20)
//make.right.equalTo(contentView.snp.rightMargin).offset(-20)
//make.right.equalToSuperview().offset(-20)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Without applying any constraints the imageView is displayed top left in the cell but applying any constraints makes the image disappear. Inspecting the collectionViewCell in debug view shows the imageView and constraints but the 'Size and horizontal position are ambiguous for UIImageView'.
I have also tried setting contentView.translatesAutoresizingMaskIntoConstraints = false amongst other things but the same results.
I'm using all code and no storyboard UI or layouts.
Thanks for any help!
You can't use both auto layout (SnapKit) and manual layout (set frame). Usually, using auto layout will cause manual layout to fail. However, your code of the constraints in snp closure is not complete. Whether using auto layout or manual layout, finally the view should get the position and size.
So, you can try like this:
imageView.snp.makeConstraints { make in
// set size
make.size.equalTo(CGSize(width: imageWidth, height: imageHeight))
// set position
make.top.equalTo(20)
make.centerX.equalTo(contentView)
}
I don't know much about SnapKit but it looks like you're missing a constraint. You need height, width, x and y. Works nicely with IOS 9 constraints.
let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.backgroundColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
self.addSubview(imageView)
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 2/3, constant: -10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1/2, constant: -10).isActive = true
}

Resources