I am trying to add all style info into a class and then subclass a UIButton to avoid duplication of code.
At the moment, my class looks like:
class CustomButton: UIButton {
required init() {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
frame.size = CGSize(width: 700, height: 100)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In the viewDidLoad I am adding:
let b1 = CustomButton()
view.addSubview(b1)
// auto layout
b1.translatesAutoresizingMaskIntoConstraints = false
b1.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
b1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
As you can see in the class I have set the frame.size
frame.size = CGSize(width: 700, height: 100)
However, when I run it, It looks like so:
The width is clearly not 700. Any suggestions where I am going wrong?
The problem is you are mixing frame and auto layout. Specifically, once you turn off autoresizing, the frames go away. Why not do 100% auto layout? In fact, why not make centerX/centerY part of the init()?
class CustomButton: UIButton {
required init(width:CGFloat, height:CGFloat, centerButton:Bool) {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: width).isActive = true
self.heightAnchor.constraint(equalToConstant: height).isActive = true
if centerButton {
self.centerXAnchor.constraint(equalTo: superview?.centerXAnchor).isActive = true
self.centerYAnchor.constraint(equalTo: superview?.centerYAnchor).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And change your call in viewDidLoad() to:
let b1 = CustomButton(width:700, height:100, centerButton:true)
(I added the width/height specs to init() to make your code more flexible.)
EDIT: Regards to my last (parenthesized) statement. If all you do is replace:
frame.size = CGSize(width: 700, height: 100)
with:
self.widthAnchor.constraint(equalToConstant: 700).isActive = true
self.heightAnchor.constraint(equalToConstant: 100).isActive = true
Everything will work. But, in your question you also said that (emphasis mine):
I am trying to add all style info into a class and then subclass a
UIButton to avoid duplication of code.
While the code is untested, I added parameters to the init in an effort to make the code more adaptable to creating buttons on the fly. Depending on your needs, you can extend this to everything from backgroundColor to cornerRadius.
I come from an OOP background (actually a top-down back in the 70s), so subclassing is way too intuitive to me. What I presented was just that - subclass to avoid duplication of code. Swift presents new ways - specifically extension and convenience init. I think both of these would even work for you. I'm not sure of the specific pros/cons of extension versus subclassing - my feeling is that duplication of code is about the same (technically) for both - but I'll always appreciate what a "modern" language brings to a developer's toolkit!
To decide any views position or size ,you can use either
Frames based layout or constrain based Autolayout not both together.
Thanks
Related
On iOS 11 many of our layouts are breaking due to labels apparently misreporting their intrinsicContentSize.
The bug seems to manifest worst when a UILabel is wrapped in another view that attempts to implement intrinsicContentSize itself. Like so (simplified & contrived example):
class LabelView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
self.label.textColor = .black
self.label.backgroundColor = .green
self.backgroundColor = .red
self.label.numberOfLines = 0
self.addSubview(self.label)
self.label.translatesAutoresizingMaskIntoConstraints = false
self.label.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
self.label.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor).isActive = true
self.label.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
self.label.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
override var intrinsicContentSize: CGSize {
let size = self.label.intrinsicContentSize
print(size)
return size
}
}
The intrinsicContentSize of the UILabel is very distinctive and looks something like: (width: 1073741824.0, height: 20.5). This causes the layout cycle to give far too much space to the view's wrapper.
This only occurs when compiling for iOS 11 from XCode 9. When running on iOS 11 compiled on the iOS 10 SDK (on XCode 8).
On XCode 8 (iOS 10) the view is rendered correctly like so:
on XCode 9 (iOS 11) the view is rendered like this:
A Gist with full playground code demonstrating this issue is here.
I have filed a radar for this and have at least one solution to the problem (see answer below). I wonder if anyone else has had this problem or has alternative approached you might try.
So through experimenting on the playground I was able to come up with a solution that involves testing for the extremely large intrinsic content size.
I noticed that all UILabels that misbehave have numberOfLines==0 and preferredMaxLayoutWidth=0. On subsequent layout passes, UIKit sets preferredMaxLayoutWidth to a non-zero value, presumably to iterate onto the correct height for the label. So the first fix was to try temporarily setting numberOfLines when (self.label.numberOfLines == 0 && self.label.preferredMaxLayoutWidth == 0).
I also noticed that all UILabels that have these two properties as 0 do not necessarily misbehave. (i.e. the inverse isn't true). So this fix worked, but modified the label unnecessarily some of the time. It also has a small bug that when the label's text contains \n newlines, number of lines should be set to the number of lines in the string, not 1.
The final solution I came to is a little more hacky, but specifically looks for UILabel misbehaving and only kick's it then...
override var intrinsicContentSize: CGSize {
guard super.intrinsicContentSize.width > 1000000000.0 else {
return super.intrinsicContentSize
}
var count = 0
if let text = self.text {
text.enumerateLines {(_, _) in
count += 1
}
} else {
count = 1
}
let oldNumberOfLines = self.numberOfLines
self.numberOfLines = count
let size = super.intrinsicContentSize
self.numberOfLines = oldNumberOfLines
return size
}
You can find this as a Gist here.
Apple's tutorial describes the difference between init(frame:) and init?(coder:) as
You typically create a view in one of two ways: by programatically
initializing the view, or by allowing the view to be loaded by the
storyboard. There’s a corresponding initializer for each approach:
init(frame:) for programatically initializing the view and
init?(coder:) for loading the view from the storyboard. You will need
to implement both of these methods in your custom control. While
designing the app, Interface Builder programatically instantiates the
view when you add it to the canvas. At runtime, your app loads the
view from the storyboard.
I feel so confused by the description "programtically initializing" and "loaded by the storyboard". Say I have a subclass of UIView called MyView, does "programtically initialization" mean I write code to add an instance of MyView to somewhere like:
override func viewDidLoad() {
super.viewDidLoad()
let myView = MyView() // init(frame:) get invoked here??
}
while init?(coder:) get called when in Main.storyboard I drag a UIView from object library and then in the identity inspector I set its class to MyView?
Besides, in my xcode project, these two methods end up with different layout for simulator and Main.storyboard with the same code:
import UIKit
#IBDesignable
class RecordView: UIView {
#IBInspectable
var borderColor: UIColor = UIColor.clear {
didSet {
self.layer.borderColor = borderColor.cgColor
}
}
#IBInspectable
var borderWidth: CGFloat = 20 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable
var cornerRadius: CGFloat = 100 {
didSet {
layer.cornerRadius = cornerRadius
}
}
private var fillView = UIView()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFillView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupFillView()
}
private func setupFillView() {
let radius = (self.cornerRadius - self.borderWidth) * 0.95
fillView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
fillView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
fillView.layer.cornerRadius = radius
fillView.backgroundColor = UIColor.red
self.addSubview(fillView)
}
override func layoutSubviews() {
super.layoutSubviews()
}
func didClick() {
UIView.animate(withDuration: 1.0, animations: {
self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
}) { (true) in
print()
}
}
}
Why do they behave differently? (I drag a UIView from object library and set its class to RecordView)
I feel so confused by the description "programtically initializing" and "loaded by the storyboard".
Object-based programming is about classes and instances. You need to make an instance of a class. With Xcode, there are two broadly different ways to get an instance of a class:
your code creates the instance
you load a nib (such a view controller's view in the storyboard) and the nib-loading process creates the instance and hands it to you
The initializer that is called in those two circumstances is different. If your code creates a UIView instance, the designated initializer which you must call is init(frame:). But if the nib creates the view, the designated initializer that the nib-loading process calls is init(coder:).
Therefore, if you have a UIView subclass and you want to override the initializer, you have to think about which initializer will be called (based on how the view instance will be created).
First your delineation between init?(coder:) and init(frame:) is basically correct. The former is used when instantiating a storyboard scene when you actually run the app, but the latter is used when you programmatically instantiate it with either let foo = RecordView() or let bar = RecordView(frame: ...). Also, init(frame:) is used when previewing #IBDesignable views in IB.
Second, regarding your problem, I'd suggest you remove the setting of the center of fillView (as well as the corner radius stuff) from setupFillView. The problem is that when init is called, you generally don't know what bounds will eventually be. You should set the center in layoutSubviews, which is called every time the view changes size.
class RecordView: UIView { // this is the black circle with a white border
private var fillView = UIView() // this is the inner red circle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFillView()
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupFillView()
}
private func setupFillView() {
fillView.backgroundColor = .red
self.addSubview(fillView)
}
override func layoutSubviews() {
super.layoutSubviews()
let radius = (cornerRadius - borderWidth) * 0.95 // these are not defined in this snippet, but I simply assume you omitted them for the sake of brevity?
fillView.frame = CGRect(origin: .zero, size: CGSize(width: radius * 2, height: radius * 2))
fillView.layer.cornerRadius = radius
fillView.center = CGPoint(x: bounds.midX, y: bounds.midY)
}
}
I would like to programmatically customize the UIButton. My code starts here:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
...
}
}
Now I would like to define a constant width and height for the button, how to do it in code?
I would recommend to use autolayout:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
// autolayout solution
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: 200).isActive = true
self.heightAnchor.constraint(equalToConstant: 35).isActive = true
}
}
You need to override override init(frame: CGRect) method
class MyButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
// Set your code here
let width = 300
let height = 50
self.frame.size = CGSize(width: width, height: height)
backgroundColor = .red
layer.shadowRadius = 5.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If your button is having constraints set from the storyboard as below and you want to change the width of the button, then this answer is helpful.
constraints set from the storyboard
Safe Area.trailing = Button.trailing + 20
Button.leading = Safe Area.leading + 20
Safe Area.bottom = Button.bottom + 20
height = 40
see the image for a better understanding.
Requirement :
if #condition 1 gets satisfied, then change button width to 100 or any width dimension.
else
if #condition 2 gets satisfied, then keep width as it is ( as per given constraints)
To handle this,
Create an IBOutlet of leading and trailing constraints of that button.
Set it to inactive.
Add width anchor for the button programmatically.
Setting it inactive is mandatory because both won't work at the same time, so be careful.
#IBOutlet weak var btnNextTrailingConstraint: NSLayoutConstraint!
#IBOutlet weak var btnNextLeadingConstraint: NSLayoutConstraint!
if (condition1) {
btnNextLeadingConstraint.isActive = false
btnNextTrailingConstraint.isActive = false
btnNext.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
else {
btnNextLeadingConstraint.isActive = true
btnNextTrailingConstraint.isActive = true
}
swift ios autolayout constraints
I tried finding a way to create a square button box. Hope I`m not at the wrong place here, but when I tried to find my own solution it was as easy as this:
button.frame.size.width = 200
button.frame.size.height = 200
And this works of course with all the other views.
I have a subclass of UIButton:
class ColorButton: UIButton {
override func awakeFromNib() {
self.layer.backgroundColor = UIColor.blackColor().CGColor
self.layer.cornerRadius = frame.size.width / 2
self.clipsToBounds = true
}
}
In interface builder, I set the button with 4 constraints: width = 100, height = 100, centerX, centerY.
The button disappears when I run my code on the simulator. However, if it set
self.layer.cornerRadius = 50
it works. I cannot figure it out. If anybody understand this problem, please tell me.
Add in awakeFromNib first line:
self.layoutIfNeeded()
Code:
class ColorButton: UIButton {
override func awakeFromNib() {
self.layoutIfNeeded()
self.layer.backgroundColor = UIColor.blackColor().CGColor
self.layer.cornerRadius = frame.size.width / 2
self.clipsToBounds = true
}
}
Your code works just fine in a fresh project so I suspect the problem is somewhere else. You forgot to call super.awakeFromNib() though. From Apple docs:
You must call the super implementation of awakeFromNib to give parent
classes the opportunity to perform any additional initialization they
require. Although the default implementation of this method does
nothing, many UIKit classes provide non-empty implementations. You may
call the super implementation at any point during your own
awakeFromNib method.
I have a custom UICollectionViewCell in my tvOS app. It has a UIImageView and some UILabels in it. I can get the cell to be focused by implementing the UIFocusEnvironment protocol without any issue, but I can't figure out how to give my custom cell the focused appearance. (Elevation and responding to user movement on the touchpad).
I'm aware of UIImageView's adjustsImageWhenAncestorFocused property, but that only elevates the image in my cell, not the entire cell.
Is there a way to make tvOS apply the (seemingly) standard focus appearance/behavior to my custom view or do I have to do it all manually?
Thanks in advance.
Update in tvOS 12.0+: Check out new classes Apple has provided in TVUIKit! You can now make custom views that have this focusing behavior!
————
I asked the same question on the Apple developer forums. Apple staff answered:
For custom views you'll have to implement the focus appearance
yourself. In the focus update method you can do things like apply a
transform and use the UIMotionAffect API.
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
if (context.nextFocusedView == self) {
// handle focus appearance changes
}
else {
// handle unfocused appearance changes
}
}
I think it'd be pretty helpful to make a UIView extension to be able to apply the same behavior to any custom view.
Maybe they'd like for us to implement more interesting ways to display focus to the user? That'd be a good reason to enable this easily only for UIImageView (Not to mention that this behavior also adds simulated light over the UIImageView, which is beautiful, but maybe only makes sense for images).
As specified in previous answers, there is no standard way, however there are 3 options for you:
(RECOMMENDED) Implement your own custom focus behaviour , that is similar to UIImageView tilting likewise:
class MotionView: UIView {
let motionEffectGroup = UIMotionEffectGroup()
override init(frame: CGRect = .zero) {
super.init(frame:frame)
self.backgroundColor = .red
addFocusMotionEffect()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addFocusMotionEffect() {
let min = CGFloat(-15)
let max = CGFloat(15)
let xMotion = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.x", type: .tiltAlongHorizontalAxis)
xMotion.minimumRelativeValue = min
xMotion.maximumRelativeValue = max
let yMotion = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.y", type: .tiltAlongVerticalAxis)
yMotion.minimumRelativeValue = min
yMotion.maximumRelativeValue = max
motionEffectGroup.motionEffects = [xMotion,yMotion]
self.addMotionEffect(motionEffectGroup)
}
func removeFocusMotionEffect() {
UIView.animate(withDuration: 0.5) {
self.removeMotionEffect(self.motionEffectGroup)
}
}
Make UIImageView to be dominant View in your cell contentView and then append your custom view to imageView's overlayContentetView so that your customView will animate alongside your UIImageView as follows:
self.imageView.overlayContentView.addSubview(logoView)
Add suitable element from TVUIKit that has the behaviour, currently as of 2022, the TVCardView servers purpose well. Then add CardView as subview of your UICollectionViewCell on top of UIImageView or TVPosterView and it will coordinate its animations with them. You need to add your custom View as subview of TVCardViews contentView. The downfall is that TVCardView cannot really have clear background and you also canot change its round corners.
class CardView: TVCardView{
override init(frame: CGRect = .zero) {
super.init(frame:frame)
let lbl = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
lbl.text = "Test Label"
self.contentView.addSubview(lbl)
self.cardBackgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}