What can cause UIStackView subviews to collapse? - uikit

I'm programmatically building a vertical UIStackView to show 3 short UILabel's:
import UIKit
import PlaygroundSupport
let box = UIView(frame: CGRect(origin: CGPoint(x:0, y:0), size: CGSize(width: 100, height: 100)))
box.backgroundColor = .white
let labels: [UILabel] = (1...3).map{ index in
let label = UILabel()
label.text = "item \(index)"
return label
}
let sv = UIStackView(arrangedSubviews: labels)
sv.axis = .vertical
sv.alignment = .center
sv.translatesAutoresizingMaskIntoConstraints = false
box.addSubview(sv)
sv.centerXAnchor.constraint(equalTo: box.centerXAnchor).isActive = true
sv.centerYAnchor.constraint(equalTo: box.centerYAnchor).isActive = true
// performing this font change on a single item collapses the stack view
if let label = sv.subviews[1] as? UILabel {
label.font = UIFont.italicSystemFont(ofSize: UIFont.labelFontSize)
}
PlaygroundPage.current.liveView = box
Performing font change of a single label from the stack view as shown above makes the labels collapse onto each other turning into unreadable blob of letters:
Instead of
I get
Am I missing some basic required constraint?
I should mention I'm using Xcode 10.2.1 Playground to develop this view.

Related

How can we make a spacer view with automatic width between two views inside UIStackview?

If we have 2 labels inside UIStackView, and I want to set a flexible width between them like this image:
what is the best way? in SwiftUI there is a function named: Spacer() , is there something similar in UIKit?
You can add a UIView to the stack to push each view in the UIStackView:
hStack:
hStack.axis = .horizontal
hStack.addArrangedSubview(label1)
hStack.addArrangedSubview(UIView.spacer(for: .horizontal))
hStack.addArrangedSubview(label2)
constraints:
hStack.translatesAutoresizingMaskIntoConstraints = false
hStack.topAnchor.constraint(equalTo: "depending on your view").isActive = true
hStack.bottomAnchor.constraint(equalTo: "depending on your view").isActive = true
hStack.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
and the extension to make this work:
// MARK: - Spacer
extension UIView {
static func spacer(size: CGFloat = .greatestFiniteMagnitude, for layout: NSLayoutConstraint.Axis = .horizontal) -> UIView {
let spacer = UIView()
if layout == .horizontal {
let constraint = spacer.widthAnchor.constraint(equalToConstant: size)
constraint.priority = .defaultLow
constraint.isActive = true
} else {
let constraint = spacer.heightAnchor.constraint(equalToConstant: size)
constraint.priority = .defaultLow
constraint.isActive = true
}
return spacer
}
}
Side note:
You may get an error that
This NSLayoutConstraint is being configured with a constant that
exceeds internal limits
... so I would rather recommend creating a UIView as a container and then pin each label to the sides of the container (leading, trailing) where each label also has a widthAnchor (if you don't want the text to extend).

How to add UIImageViews to a UIStackView that is constrained to the centerXAnchor of a view?

I'm trying to add profile icons via UIImageViews to a UIStackView in order to keep the icons centered in a view. How would I go about adding UIImageViews of a fixed frame to a UIStackView and keep the UIStackView centered in the main view according to varying numbers of UIImageViews in the UIStackView?
let memberIcons: UIStackView = {
let iconView = UIStackView()
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.axis = .horizontal
iconView.spacing = 5
iconView.distribution = .equalSpacing
iconView.alignment = .center
return iconView
}()
for member in story!.members {
let circle = UIImageView()
circle.frame = CGRect(x: 0, y: 0, width: 36, height: 36)
circle.translatesAutoresizingMaskIntoConstraints = false
circle.layer.cornerRadius = CGFloat(circle.frame.width / 2)
circle.image = member.profilePicture
circle.contentMode = .scaleAspectFill
circle.clipsToBounds = true
memberIcons.addArrangedSubview(circle)
}
Because you set memberIcons.distribution = .equalSpace, the stack view will ask its subviews for their intrinsic sizes. When asked, the UIImage (i.e. circle) will calculate its intrinsic size as "image pixel size / scale", which is not what you want -- you want the image to be of fixed size (36 x 36).
Use Auto Layout on circle:
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(memberIcons)
memberIcons.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
memberIcons.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Limit the stack view's width to no more than 75% of the superview's width
// Adjust as needed
memberIcons.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, multiplier: 0.75).isActive = true
let width: CGFloat = 36.0
for member in story!.members {
// We don't care about the frame here as we're gonna use auto layout
let circle = UIImageView(frame: .zero)
circle.translatesAutoresizingMaskIntoConstraints = false
circle.layer.cornerRadius = width / 2
circle.image = member.profilePicture
circle.contentMode = .scaleAspectFill
circle.clipsToBounds = true
circle.layer.borderWidth = 1
circle.layer.borderColor = UIColor.lightGray.cgColor
memberIcons.addArrangedSubview(circle)
circle.widthAnchor.constraint(equalToConstant: width).isActive = true
circle.heightAnchor.constraint(equalToConstant: width).isActive = true
}
}
Result:
Because we limit the width of the UIStackView, there a maximum number of profile images you can add (7 in this case) before you get a bunch of auto layout error on the console. You can enclose the Stack View inside a Scroll View or use a Collection View for a matrix-like display.

macOS Swift CALayer.contents does not load image

The code below creates a red rectangle that is animated to move across the view from left to right. I would like to have an arbitrary shape loaded from an image to either superimpose or replace the rectangle. However, the circleLayer.contents = NSImage statement in the initializeCircleLayer function doesn't produce any effect. The diagnostic print statement seems to verify that the image exists and has been found, but no image appears in the view. How do I get an image into the layer to replace the animated red rectangle? Thanks!
CODE BELOW:
import Cocoa
class ViewController: NSViewController {
var circleLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
initializeCircleLayer()
simpleCAAnimationDemo()
}
func initializeCircleLayer(){
circleLayer.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
circleLayer.position = CGPoint(x: 50, y: 150)
circleLayer.backgroundColor = NSColor.red.cgColor
circleLayer.cornerRadius = 10.0
let testIm = NSImage(named: NSImage.Name(rawValue: "testImage"))
print("testIm = \(String(describing: testIm))")
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage
circleLayer.contentsGravity = kCAGravityCenter
self.view.layer?.addSublayer(circleLayer)
}
func simpleCAAnimationDemo(){
circleLayer.removeAllAnimations()
let animation = CABasicAnimation(keyPath: "position")
let startingPoint = NSValue(point: NSPoint(x: 50, y: 150))
let endingPoint = NSValue(point: NSPoint(x: 600, y: 150))
animation.fromValue = startingPoint
animation.toValue = endingPoint
animation.repeatCount = Float.greatestFiniteMagnitude
animation.duration = 10.0
circleLayer.add(animation, forKey: "linearMovement")
}
}
Why it doesn't work
The reason why
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage
doesn't work is because it's a reference to the cgImage(forProposedRect:context:hints:) method, meaning that its type is
((UnsafeMutablePointer<NSRect>?, NSGraphicsContext?, [NSImageRep.HintKey : Any]?) -> CGImage?)?
You can see this by assigning NSImage(named: NSImage.Name(rawValue: "testImage"))?.cgImage to a local variable and ⌥-clicking it to see its type.
The compiler allows this assignment because circleLayer.contents is an Any? property, so literally anything can be assigned to it.
How to fix it
As of macOS 10.6, you can assign NSImage objects to a layers contents directly:
circleLayer.contents = NSImage(named: NSImage.Name(rawValue: "testImage"))

UIButton not visible in UIScrollView with UIPageControl - Swift 3

I am dynamically creating a scrollView and having a page control associated with it.I have an array of buttons and these buttons is being added to the scroll view dynamically.While the buttons thats are added in the first page are visible , some buttons are not visible , but they are being added to the Scroll View.
UIScrollView frame = CGRect(x: 0, y: 0, width: 340, height: 150)
scrollView.contentSize = CGSize(width: scrollView.frame.size.width * CGFloat(page), height: scrollView.frame.size.height)
In my case , I have 2 pages , so the content width is 680.
On scrolling the view , the content offset of the scroll view is 340 , although the the buttons that are invisible are within the contentSize , the buttons are still not visible ,
Following are the frames of the button that are not visible ,
<UIButton: 0x7ffdf0f26bb0; frame = (355 10; 62 32); opaque = NO; layer = <CALayer: 0x610000030840>>,
<UIButton: 0x7ffdf0f27110; frame = (427 10; 98 32); opaque = NO; layer = <CALayer: 0x610000030960>>,
<UIButton: 0x7ffdf0f27670; frame = (535 10; 112 32); opaque = NO; layer = <CALayer: 0x610000030a80>>
As far as page control is concerned , even if i remove the page control , I see the same behavior in the scrollview.
I am not sure if i have to check for anything specific on the scrollview.
#IBAction func changePage(sender: UIPageControl) -> () {
let x = CGFloat(pageControl.currentPage + 1) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x,y :scrollView.frame.size.height), animated: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
}
The issues was not with the page control or scrollview , I was using an extension to round corners of the scrollView which was causing this issue ,
extension UIView {
func roundCorners(corners:UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}}
I still don't understand why adding mask to the layer was messing the scrollView.

Views and subviews

I'm trying to put a view with subviews. The view has an image and a text, which are subviews, but when i run the app, it doesn't show the complete text. Here is the code…
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let ballonview = UIImageView()
let label = UILabel()
let turn = 1
ballonview.frame = CGRectZero
label.frame = CGRectZero
label.backgroundColor = UIColor.clearColor()
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.ByWordWrapping
label.font = label.font.fontWithSize(20)
let message = UIView()
message.frame = CGRectMake(-160, 48, view.frame.size.width, view.frame.size.height)
message.addSubview(ballonview)
message.addSubview(label)
let text: NSString = "hola como estas"
let size:CGSize = text.boundingRectWithSize(CGSizeMake(240.0, 480.0), options: NSStringDrawingOptions.UsesDeviceMetrics, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(14.0)], context: nil).size
var ballon:UIImage = UIImage()
ballonview.frame = CGRectMake(320.0 - (size.width + 28.0), 2.0, size.width + 28.0, size.height + 15.0)
ballon = UIImage(named:"green.png")!.stretchableImageWithLeftCapWidth(24, topCapHeight: 15)
label.frame = CGRectMake(307.0 - (size.width + 5.0), 8.0, size.width + 5.0, size.height)
ballonview.image = ballon;
label.text = text as String
view.addSubview(message)
}
What can I do?
The problem here is that you are using different font sizes when measuring the size and rendering.
The font of label is 20pt system font, and the font you use for measuring size is 14pt system font. So the size will be smaller than you expected.
Also, the drawing options should be changed to NSStringDrawingUsesLineFragmentOrigin in order to get the correct size if you have characters with long legs (like g, j, y).
Reference: official NSString document.
I'd suggest you change the way you measure the size:
let size: CGSize = text.boundingRectWithSize(
CGSizeMake(240.0, 480.0),
options: .UsesLineFragmentOrigin,
attributes: [
NSFontAttributeName: label.font
],
context: nil
).size
So it will match the font you use for rendering the label.
I hope this answer helps you.

Resources