UITextField border with AutoLayout is flickering - ios

I am designing screen like a form, containing few UITextFields using AutoLayout. I wanted to set border only at bottom of the UITextFields. I have set border using CALayer. But UITextField occupies its height(after autolayout is applied to it) in method viewDidAppear, so adding border to UITextField in viewDidAppear makes it appear as if its flickering. So Is there any other way to set border to UITextFeild at bottom with AutoLayout.

class CustomTextField: UITextField {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
func commonInit() {
self.borderStyle = .none //To remove default border.
let bottomBorder = UIView()
bottomBorder.frame.size = CGSize(width: self.frame.size.width, height: 1)
bottomBorder.frame.origin = CGPoint(x: 0, y: self.frame.size.height - 1)
bottomBorder.backgroundColor = UIColor.lightGray
bottomBorder.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
self.addSubview(bottomBorder)
}
}
Finally i achieved it by creating CustomUITextField with AutoLayout. Just apply above class to your UITextField in interface builder.

If I'm understanding you, all you have to do to solve your problem is to call the method "to draw the border" in the method above instead of viewDidAppear. I guess it can sove your problem.
override func viewDidLayoutSubviews() {
//A UIViewController's overrode method
//call you method here
}
This method is called before the view appears, and after the layout of the subviews. When you're working - changing I mean - layers, you should always use it instead of viewDidLoad, for example.
Hope it helps :)

Steps - Select the textfield -> show attribute inspector right panel and select dotted border style .
Next drag the uilabel into the scene and make it 1.0 and width what do you want, keep the bottom of the textfield. So your problem is solved. This may help you.

If you are using the simulator to test that. It looks like its flickering but its not.
If you are scaling the simulator to 25%. The 1px lines appears and
disappears when you scroll cause the screen resolution you have is
less than the real device resolution.
Test it while scaling the simulator to 100%. cmd+1

Related

How can I set an underline on a UITextField?

I am trying to set an underline on my UITextFields. I have tried a couple of methods but none of them seem to work. After looking through a couple of websites, the most suggested method is the following:
extension UITextField {
func setUnderLine() {
let border = CALayer()
let width = CGFloat(0.5)
border.borderColor = UIColor.lightGray.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width-10, height: self.frame.size.height)
border.borderWidth = width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
}
}
I can't think of any reason as to why the code above would not work, but all the answers I saw were posted a couple of years ago.
Could someone please let me know what I am doing wrong?
One problem I see with the code that you posted is that it won't update the layer if the text field gets resized. Each time you call the setUnderLine() function, it adds a new layer, then forgets about it.
I would suggest subclassing UITextField instead. That code could look like this:
class UnderlinedTextField: UITextField {
let underlineLayer = CALayer()
/// Size the underline layer and position it as a one point line under the text field.
func setupUnderlineLayer() {
var frame = self.bounds
frame.origin.y = frame.size.height - 1
frame.size.height = 1
underlineLayer.frame = frame
underlineLayer.backgroundColor = UIColor.blue.cgColor
}
// In `init?(coder:)` Add our underlineLayer as a sublayer of the view's main layer
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(underlineLayer)
}
// in `init(frame:)` Add our underlineLayer as a sublayer of the view's main layer
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(underlineLayer)
}
// Any time we are asked to update our subviews,
// adjust the size and placement of the underline layer too
override func layoutSubviews() {
super.layoutSubviews()
setupUnderlineLayer()
}
}
That creates a text field that looks like this:
(And note that if you rotate the simulator to landscape mode, the UnderlineTextField repositions the underline layer for the new text field bounds.)
Note that it might be easier to just add a UIView to your storyboard, pinned to the bottom of your text field and one pixel tall, using your desired underline color. (You'd set up the underline view using AutoLayout constraints, and give it a background color.) If you did that you wouldn't need any code at all.
Edit:
I created a Github project demonstrating both approaches. (link)
I also added a view-based underline to my example app. That looks like this:

Custom View - self.frame is not correct?

So I have a custom UIView class
class MessageBox: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubViews()
}
func createSubViews() {
let testView = UIView(frame: self.frame)
testView.backgroundColor = UIColor.brown
self.addSubview(testView)
}
}
I added a UIView inside the storyboard and gave it some constraints:
100 from the top (superview), 0 from the left and right, height is 180
But when I run the app the brown subview I created in the code is way to big. I printed self.frame in my custom view and it turns out that the frame is (0,0,1000,1000). But why? I set constraints, it should be something like (0,0,deviceWith, 180).
What did I do wrong?
EDIT: That's my Storyboard setup:
Short and simple answer:
You're doing it too early.
Detailed answer:
When a view is initialized from an Interface Builder file (a xib or a storyboard) its frame is initially set to the frame it has in Interface Builder. You can look at it as a temporary placeholder.
When using Auto Layout the constraints are resolved (= the view's actual frame is computed) inside the view's layoutSubviews() method.
Thus, there are two possible solutions for your problem:
(preferrable) If you use Auto Layout, use it throughout your view.
Either add your testView in Interface Builder as well and create an outlet for it
or create your testView in code as you do, then set its translatesAutoResizingMaskIntoConstraints property to false (to sort of "activate Auto Layout") and add the required constraints for it in code.
Set your testView's frame after the MessageBox view's frame itself has been set by the layout engine. The only place where you can be sure that the system has resolved the view's frame from the constraints is when layoutSubviews() is called.
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
(You need to declare your testView as a property / global variable, of course.)
Try to use the anchors for your view:
MessageBox.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor).active
= true
MessageBox.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor).active
= true
MessageBox.widthAnchor.constraintEqualToConstant(150).active = true
MessageBox.heightAnchor.constraintEqualToConstant(100).active = true
This method have to be used inside your class
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
this also works when you add a custom class to a UIView in the storyboard and that uses autolayout.
thanks Mischa !
try to add a height and width constraint relative to the superview height, with some multiplier.

Animate/zoom a focused custom view in tvOS

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")
}
}

Best place to set cornerRadius based on UIButton size in subclass?

So I will be having a few different rounded buttons within an app (all the way to circular buttons), and from what I can tell, the easiest way to achieve this is to set the cornerRadius property of the buttons CALayer.
However, I don't want to be doing this manually for every button that requires it in every controller, so I thought a simple subclass that sets this on init would be the way.
I am using Storyboard and Autolayout to position and size the buttons, and assigning them to this subclass.
class RoundedButton: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = self.bounds.size.height / 2.0
self.clipsToBounds = true
NSLog("BUTTON BOUNDS: H-%f W-%f", self.bounds.size.height, self.bounds.size.width)
NSLog("BUTTON FRAME: H-%f W-%f", self.frame.height, self.frame.width)
}
}
But I have come to find out that at this point (i.e. init), the size of neither the frame nor bounds are final. For a button in Storyboard sized (and constrained) to H40 x W40, the bounds/frame sizes are showing H30 x W38.
This means that cornerRadius doesn't get the value I expect.
I have confirmed that at some later point (e.g. when the button can already respond to a tap) that its frame/bounds are indeed H40 x W40.
So after all that, my question is, within the UIButton subclass, when/where can I safely set cornerRadius using the final frame/bounds values of the instance?
If you want your code to be executed after the view has gone through AutoLayout you must do it in layoutSubviews after calling super.layoutSubviews().
Like this :
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height / 2.0
clipsToBounds = true
}
}
This code is not perfect though because it doesn't support when height is bigger than width (easy to fix though…).
Try this in Button class.
override func layoutSubviews() {
super.layoutSubviews()
// here...
}
Since you wish to use initWithCoder rather than initWithFrame (where you do have the frame struct), you can override the layoutSubviews method.
When layoutSubviews is called the frame is correct.
In objective-c you can write
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.bounds.size.height/2;
}

iOS Keyboard - input Accessory View xib with autolayout not receiving touch event

I've built a growing UITextView attached to the keyboard, similar to the stock Messages app, by loading a .xib into the keyboard's inputAccessoryView as such:
self.keyboardAccessoryView = [[[NSBundle mainBundle]
loadNibNamed:#"KeyboardAccessoryView"
owner:self options:nil]
firstObject];
The .xib looks like this, and is using layout constraints so that the textView grows vertically when the user enters more lines of text:
This is all working great, with rotation and everything, except one big bug -- when the text is multiple lines, only the very bottom line handles touch events. This means that a user cannot scroll inside the UITextView because their touch events are being passed to the (dark gray) view in the back and scrolling that instead. They also cannot select and edit their text on the top 3 lines.
I think I could do a workaround by capturing the coordinates of all tap events and checking if the keyboard is open and how tall the UITextView is, then selecting the correct element to receive the touch event. But this is brittle solution that is more complicated with rotation. Is there something I'm missing in my auto-growing text view approach, or some easier fix?
To make input accessory view grow vertically you just set its autoresizingMask = .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.
The code:
class InputAccessoryView: UIView, UITextViewDelegate {
let textView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
// This is required to make the view grow vertically
self.autoresizingMask = UIViewAutoresizing.flexibleHeight
// Setup textView as needed
self.addSubview(self.textView)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.textView.delegate = self
// Disabling textView scrolling prevents some undesired effects,
// like incorrect contentOffset when adding new line,
// and makes the textView behave similar to Apple's Messages app
self.textView.scrollEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
// Calculate intrinsicContentSize that will fit all the text
let textSize = self.textView.sizeThatFits(CGSize(width: self.textView.bounds.width, height: CGFloat.max))
return CGSize(width: self.bounds.width, height: textSize.height)
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
// Re-calculate intrinsicContentSize when text changes
self.invalidateIntrinsicContentSize()
}
}
This approach is quite straightforward and reliable, as it doesn't require hacking constraints or recreating the view each time its size changes.
I figured out that even though the keyboard accessory input grows vertically with auto layout, its frame does not. So you have to adjust the keyboard accessory's frame each time the height of the uitextview grows, shrinks, and rotates. This introduces some complications as UITextView's in iOS7 are notoriously buggy -- I noticed behavior was not consistent across iPhone, iPad, and the Simulator.

Resources