Setting view width constraint from custom UIView class not updating frames - swift

I've got a UIView in which I want to have a simple loader view by using a percentage to determine the width.
I've got the percentage code completed and I know it's working.
However I'm having trouble setting the view's width constraint. I find the width by getting the frame width and multiplying it by the percentage. I know it's getting the right width. But I can't seem to set the constraint from this function.
My code goes like this in my UIView subclass:
func initSubviews() {
// in here i do some other stuff and have an asyncronous call to an api
// so then I've got this code calling the next function
DispatchQueue.main.async {
self.setCompletionWidth(nextTimer: nextTimer!, oldDate: oldDate!)
}
}
func setCompletionWidth(nextTimer: Date, oldDate: Date) {
let date = Date()
// calculatePercent returns something like 0.49
let percent = calculatePercent(middleDate: date, endDate: nextTimer, originalDate: oldDate)
//this is returning a correct value
let width = (self.frame.width)*percent
// completionView is the view I'm trying to change the width of
self.completionView.widthAnchor.constraint(equalToConstant: width)
self.layoutSubviews()
}
What's happening is that the completionView isn't getting the right width.
I've also tried in the setCompletionWidth function
self.completionView.widthAnchor.constraint(equalToConstant: width)
containerView.layoutSubviews()
and
UIView.animate(withDuration: 0.2) {
self.completionView.widthAnchor.constraint(equalToConstant: width)
self.containerView.layoutSubviews()
//also tried self.layoutSubviews here
}
and
self.layoutIfNeeded()
self.completionView.widthAnchor.constraint(equalToConstant: width)
self.layoutIfNeeded()
I'm expecting the width of the completionView to be something like 150, but instead it's 350, which is the original width it had.
I think what's happening is the view isn't updating after me setting the constraint to a different value. However, I can't for the life of me get it to update. I'd love some help here.

You need .isActive = true
self.completionView.widthAnchor.constraint(equalToConstant: width).isActive = true
Also to change the width you need to create a width var like
var widthCon:NSLayoutConstraint!
widthCon = self.completionView.widthAnchor.constraint(equalToConstant: width)
widthCon.isActive = true
Then change the constant with
widthCon.constant = /// some value
self.superview!.layoutIfNeeded()

Related

Change content size UIScrollView dynamically

I have a scroll view, when the view is first loaded, the size is set dynamically, but when I click on the button, the internal size of my elements changes and I need to change the internal size of the scroll, but it does not change. Someone knows how to fix it?
DispatchQueue.main.async {
var contentRect = CGRect()
for view in self.scrollView.subviews {
contentRect = contentRect.union(view.frame)
self.scrollView.contentSize = contentRect.size
}
}
If you really don't want to use auto-layout / constraints, you can call this function each time you add (or remove) a subview from the scroll view, or after you've changed the size(s) of the subview(s):
func updateContentSize() -> Void {
// this will get the right-edge of the right-most subview
let width = scrollView.subviews.map {$0.frame.maxX}.max() ?? 0.0
// this will get the bottom-edge of the bottom-most subview
let height = scrollView.subviews.map {$0.frame.maxY}.max() ?? 0.0
// set the contentSize
scrollView.contentSize = CGSize(width: width, height: height)
}
This solution is for auto-layout/constraints.
You need a reference constraint to manipulate the height of the inner container view of the scrollview.
private var _constraintInnerContainerScroll:NSLayoutConstraint?
You need to set the initial height of the inner container view, suppose 700.0
private let _containerViewHeightFixed : CGFloat = 700.0
then you need to save the reference
_constraintInnerContainerScroll = _containerView.heightAnchor.constraint(equalToConstant: _containerViewHeightFixed)
_constraintInnerContainerScroll?.isActive = true
You initial view is setup and ready, now suppose you add 2 more subview of height 100.0 each, now your new inner container view height should be 700.0+200.0 = 900.0
if let const1 = _constraintInnerContainerScroll{
const1.constant = _containerViewHeightFixed + 200.0
UIView.animate(withDuration: 0.5) {
self?._containerView.layoutIfNeeded()
}else{
print("constraint reference not saved")
}
let me know if this works for you, or if this can be improved.

Swift 5: centerXAnchor won't center my frame in the view

I'm stuck at the Consolidation IV challenge from hackingwithswift.com.
Right now I'm trying to create a hangman game. I thought to place placeholder labels based on the length of the answer word. These placeholder labels would be placed inside a frame, which then would be placed in the center of the main view.
Unfortunately, the leading edge of the frame is placed centered. In my opinion, this is not a problem of constraints, but rather a problem of me creating the frame wrong.
My current code is
import UIKit
class ViewController: UIViewController {
var answer: String!
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - declare all the labels here
let letterView = UIView()
letterView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(letterView)
// MARK: - set constraints to all labels, buttons etc.
NSLayoutConstraint.activate([
letterView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
letterView.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
// MARK: - populate the letterView
// set the size of the placeholder
answer = "Atmosphäre"
let height = 60
let width = 25
// var width: Int
for placeholder in 0..<answer.count {
// create new label and give it a big font size
let placeholderLabel = UILabel()
placeholderLabel.text = "_"
placeholderLabel.font = UIFont.systemFont(ofSize: 36)
// calculate frame of this label
let frame = CGRect(x: placeholder * width, y: height, width: width, height: height)
placeholderLabel.frame = frame
// add label to the label view
letterView.addSubview(placeholderLabel)
}
}
}
The simulator screen looks just like this:
I already searched for answers on stackoverflow, but wasn't successful. I think I don't know what I'm exactly looking for.
The main problem, is that the letterView has no size, because no width or height constraints are applied to it.
To fix your code make the letterView big enough to contain the labels you've added as subviews by adding height and width constraints after the for loop:
for placeholder in 0..<answer.count {
...
}
NSLayoutConstraint.activate([
letterView.widthAnchor.constraint(equalToConstant: CGFloat(width * answer.count)),
letterView.heightAnchor.constraint(equalToConstant: CGFloat(height))
])
I'm not sure if you've covered this in your course yet, but a better way to go about this (which would take much less code), is to use a UIStackView as your letterView instead.
An extra thing to consider:
If you give the letterView a background color, you'll see that the labels are actually aligned outside of its bounds:
That's because you're setting each label's y position to be height, when it should probably be zero:
let frame = CGRect(x: placeholder * width, y: 0, width: width, height: height)
Correcting this places the labels within the bounds of the letterView:

Swift method that is called after autolayout is finalized

I have a method that draws pins by adding an ImageView on top of a slider when a button is pressed. Here is the code:
var loopStartImageView : UIImageView = UIImageView()
func addLoopDrawing(at time: CMTime, for loop: Int) {
let imgHeight : CGFloat = 30
let imgWidth: CGFloat = 30
var pinImg : UIImage = UIImage(named: "pin2x")!
let inset = slider.frame.origin.x
let width = slider.bounds.width
let xPos : CGFloat = CGFloat((Float(time.seconds) / slider.maximumValue)) * width + inset - (imgWidth / 2)
var yPos : CGFloat = slider.frame.origin.y - (slider.frame.height / 2) - 3
let imgV = UIImageView(frame: CGRect(x: xPos, y: yPos, width: imgWidth, height: imgHeight))
imgV.image = pinImg
loopStartImageView = imgV
view.addSubview(loopStartImageView)
view.sendSubviewToBack(loopStartImageView)
}
The drawing is correct when I don't use autolayout to set the position of the slider, but once I do it shows up below the actual positioning.
This method is called in viewDidLoad. My guess is that for some reason the auto layout positioning is not set when viewDidLoad is called.
I was wondering if there is a method that is called once auto layout is fully adjusted?
//
This is what it should look like:
But this is how the view loads (even when calling the addLoopDrawing function inside the viewDidLayoutSubviews)
I was wondering if there is a method that is called once auto layout is fully adjusted?
That would be viewDidLayoutSubviews. That is a very good time to do things that depend upon things having their correct frame, but beware: it can be called many times. The usual thing, therefore, is to implement it along with a Bool property to check that this is the first time.
On the other hand, it might be better not to ask an x-y question like this. The idea of an image view on top of a slider seems wrong. It might be better to describe what you are really trying to do.

Consistent curves with dynamic corner radius (Swift)?

Is there a way to make the corner radius of a UIView adept to the view it belongs to? I'm not comfortable with the idea of hard-coding corner radius values, because as soon as the width or the height of your view changes (on different screen orientations for example), the corners will look totally different. For example, take a look at WhatsApp's chat window.
As you can see, every message container view has a different width and a different height, but the curve of the corners are all exactly the same. This is what I'm trying to achieve. I want the curves of my corners to be the same on every view, no matter what the size of the view is or what screen the view is displayed on. I've tried setting the corner radius relative to the view's height (view.layer.cornerRadius = view.frame.size.height * 0.25) and I've also tried setting it to the view's width, but this doesn't work. The corners still look weird as soon as they are displayed on a different screen size. Please let me know if there's a certain formula or trick to make the curves look the same on every view/screen size.
Here's the best I can do. I don't know if this will be of help, but hopefully it will give you some ideas.
First the code:
class ViewController: UIViewController {
let cornerRadius:CGFloat = 10
let insetValue:CGFloat = 10
var numberOfViews:Int = 0
var myViews = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
}
override func viewWillLayoutSubviews() {
setNumberOfViews()
createViews()
createViewHierarchy()
addConstraints()
}
func setNumberOfViews() {
var smallerDimension:CGFloat = 0
if view.frame.height < view.frame.width {
smallerDimension = view.frame.height
} else {
smallerDimension = view.frame.width
}
let viewCount = smallerDimension / (insetValue * 2)
numberOfViews = Int(viewCount)
}
func createViews() {
for i in 1...numberOfViews {
switch i % 5 {
case 0:
myViews.append(MyView(UIColor.black, cornerRadius))
case 1:
myViews.append(MyView(UIColor.blue, cornerRadius))
case 2:
myViews.append(MyView(UIColor.red, cornerRadius))
case 3:
myViews.append(MyView(UIColor.yellow, cornerRadius))
case 4:
myViews.append(MyView(UIColor.green, cornerRadius))
default:
break
}
}
}
func createViewHierarchy() {
view.addSubview(myViews[0])
for i in 1...myViews.count-1 {
myViews[i-1].addSubview(myViews[i])
}
}
func addConstraints() {
for view in myViews {
view.topAnchor.constraint(equalTo: (view.superview?.topAnchor)!, constant: insetValue).isActive = true
view.leadingAnchor.constraint(equalTo: (view.superview?.leadingAnchor)!, constant: insetValue).isActive = true
view.trailingAnchor.constraint(equalTo: (view.superview?.trailingAnchor)!, constant: -insetValue).isActive = true
view.bottomAnchor.constraint(equalTo: (view.superview?.bottomAnchor)!, constant: -insetValue).isActive = true
}
}
}
class MyView: UIView {
convenience init(_ backgroundColor:UIColor, _ cornerRadius:CGFloat) {
self.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = backgroundColor
self.layer.cornerRadius = cornerRadius
}
}
Explanation:
This is fairly simple code. The intent was to create as deeply nested a view hierarchy as possible, and, using auto layout, have two main variables: cornerRadius (the view's corner radius) and insetValue (the "frame's" inset). These two variables can be adjusted for experimenting.
The bulk of the logic is in viewWillLayoutSubviews, where the root view frame size is know. Since I'm using 5 different background colors, I'm calculating how many views can fit in the hierarchy. Then I'm creating them, followed by creating the view hierarchy, and finally I'm adding the constraints.
Experimenting and conclusions:
I was able to see what your concern is - yes, if a view's size components are smaller than the corner radius, you end up with inconsistent looking corners. But these values are pretty small - pretty much 10 or less. Most views are unusable at that size. (If I recall even the HIG suggests that a button should be no less than 40 points in size. Sure, even Apple breaks that rule. Still.)
If your 'insetValueis sufficiently larger than the corner radius, you should never have an issue. Likewise, using the iMessage scenario, a singleUILabelcontaining text and/or emoticons should have enough height that a noticeablecornerRadius` can be had.
The key point to set things like cornerRadius and insetValue is in viewWillLayoutSubviews, when you can decide (1) which is the smaller dimension, height or width, (2) how deeply you can nest views, and (3) how large of a corner radius you can set.
Use auto layout! Please note the absolute lack of frames. Other than determining the root view's dimensions at the appropriate time, you can write very compact code without worrying about device size or orientation.

NSTextView not resizing properly after setFrameSize

In an NSTextView subclass I have created, I want to resize the height of the view to the height of the text within it. To execute this, I used apple's recommended procedure of counting lines within a text view:
private func countln() -> Int {
var nlines: Int
var index: Int
var range = NSRange()
let nGlyphs = lManager.numberOfGlyphs
for (nlines = 0, index = 0; index < nGlyphs; nlines++) {
lManager.lineFragmentRectForGlyphAtIndex(index, effectiveRange: &range)
index = NSMaxRange(range);
}
return nlines
}
This method works as expected and returns the correct number of lines in the text view. The issue lies in the resizing of the view, which I inserted into the delegate method that is called on text change:
func textDidChange(notification: NSNotification) {
let newHeight = CGFloat(28 * countln())
let ogHeight = self.frame.height
self.setFrameSize(NSSize(width: self.frame.width, height: newHeight))
self.setFrameOrigin(NSPoint(x: self.frame.origin.x, y: (self.frame.origin.y - self.frame.height) + ogHeight))
Swift.print(frame.height)
}
The setFrameSize variable function resizes the height of the view based not the number of lines in the view (multiplied by a constant that is more-or-less the height of each line of text). Everything works perfectly until immediately after the change of height is made, when the text view's height changes to an unanticipated incorrect height. I presume there is an issue with the frequent redrawing of the view in relation to the way I am resizing it. Any help on how to solve this issue of incorrect height resizing is greatly appreciated.
Thanks in advance.