Animate/zoom a focused custom view in tvOS - focus

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

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:

Child UIViews unable to receive touch events

I have a UIViewController that embeds a UITableView. This table view is 3/4 the size of the entire screen in height. the remaining 1/4th of the UIViewController has a rounded UIButton that triggers a new UIView on top of the parent view. (UITableView).
Upon instantiating and calling a UIView with a background that is set to:
self.backgroundColor = UIColor(black: 1, alpha: 0.5) it would normally fill the entire view with a black see-through background that will then have an additional UIView with the following constraints:
Leading: 10
Trailing: 10
Top: 50
Bottom: 50
this, in turn, gives me a 'Card' effect on top of the tableView. This 'Card' view then has a UITextview property that is supposed to show the keyboard when the user taps the view with the textview embedded.
The Problem:
Upon selecting the UITextview, or even touching this 'Card' view, the background table is being selected and interacted with. Neither the 'Card' textfield raises the keyboard nor does it make itself solely interactive as the parent table controller seems to be getting the touch events.
Is there any solution to this problem that is encountered in iOS 11? I never experienced this issue in iOS 10. I am using iPhone X for the further note on my issue.
Here is an example of the actual issue occurring.
NOTE: If I were to select the dark area of the card itself, the table view would receive touch events but not the text area within the card view. The reason this card view has the keyboard showing is that I called it manually in code with the textview.becomeFirstResponder method.
ADDITIONAL NOTES: I have also enabled the isUserInteractionEnabled = false on the overall presenting child view. Still does not work and parent view receives touches only.
Code for the Card
class Card: UIView {
// instantiating the textview
var textview = UITextview()
var cardView = UIView()
private override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(black: 1, alpha: 0.5)
self.cardView.backgroundColor = UIColor.white
self.cardView.layer.cornerRadius = 8
// Just demo the textview
self.textview.frame = CGRect(x: 10, y: 100, width: 300. height: 100)
self.addSubview(cardView) // adding the cardView as a subview to the background colored view
cardView.addSubview(textview)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Error")
}
private func ConstrainCardWith() {
// Constraints
}
}
extension UIView {
func Width()-> CGFloat {
return UIScreen.main.bounds.size.width
}
func Height()-> CGFloat{
return UIScreen.main.bounds.size.height
}
}
The question is your container view(cardview) has no frame, so your child view(textview) can not be touch cause it is out of bounds, so just add some frame in your card view will solve this question

UITextField border with AutoLayout is flickering

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

Slow load time for custom UIView in Swift

Background
In order to make a text view that scrolls horizontally for vertical Mongolian script, I made a custom UIView subclass. The class takes a UITextView, puts it in a UIView, rotates and flips that view, and then puts that view in a parent UIView.
The purpose for the rotation and flipping is so that the text will be vertical and so that line wrapping will work right. The purpose of sticking everything in a parent UIView is so that Auto layout will work in a storyboard. (See more details here.)
Code
I got a working solution. The full code on github is here, but I created a new project and stripped out all the unnecessary code that I could in order to isolate the problem. The following code still performs the basic function described above but also still has the slow loading problem described below.
import UIKit
#IBDesignable class UIMongolTextView: UIView {
private var view = UITextView()
private var oldWidth: CGFloat = 0
private var oldHeight: CGFloat = 0
#IBInspectable var text: String {
get {
return view.text
}
set {
view.text = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect){
super.init(frame: frame)
}
override func sizeThatFits(size: CGSize) -> CGSize {
// swap the length and width coming in and going out
let fitSize = view.sizeThatFits(CGSize(width: size.height, height: size.width))
return CGSize(width: fitSize.height, height: fitSize.width)
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews gets called multiple times, only need it once
if self.frame.height == oldHeight && self.frame.width == oldWidth {
return
} else {
oldWidth = self.frame.width
oldHeight = self.frame.height
}
// Remove the old rotation view
if self.subviews.count > 0 {
self.subviews[0].removeFromSuperview()
}
// setup rotationView container
let rotationView = UIView()
rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
rotationView.userInteractionEnabled = true
self.addSubview(rotationView)
// transform rotationView (so that it covers the same frame as self)
rotationView.transform = translateRotateFlip()
// add view
view.frame = rotationView.bounds
rotationView.addSubview(view)
}
func translateRotateFlip() -> CGAffineTransform {
var transform = CGAffineTransformIdentity
// translate to new center
transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
// rotate counterclockwise around center
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
// flip vertically
transform = CGAffineTransformScale(transform, -1, 1)
return transform
}
}
Problem
I noticed that the custom view loads very slowly. I'm new to Xcode Instruments so I watched the helpful videos Debugging Memory Issues with Xcode and Profiler and Time Profiler.
After that I tried finding the issue in my own project. It seems like no matter whether I use the Time Profiler or Leaks or Allocations tools, they all show that my class init method is doing too much work. (But I kind of knew that already from the slow load time before.) Here is a screen shot from the Allocations tool:
I didn't expand all of the call tree because it wouldn't have fit. Why are so many object being created? When I made a three layer custom view I knew that it wasn't ideal, but the number of layers that appears to be happening from the call tree is ridiculous. What am I doing wrong?
You shouldn't add or delete any subview inside layoutSubviews, as doing so triggers a call to layoutSubviews again.
Create your subview when you create your view, and then only adjust its position in layoutSubviews rather than deleting and re-adding it.

Animate `backgroundColor` of a `UIView` that implements `drawRect`

I have a custom UIView and I would like to animate its backgroundColor property. This is an animatable property of a UIView.
This is the code:
class ETTimerUIView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// other methods
func flashBg() {
UIView.animateWithDuration( 1.0, animations: {
self.backgroundColor = UIColor.colorYellow()
})
}
override func drawRect() {
// Something related to a timer I'm rendering
}
This code causes causes the animation to skip and the color to change immediately:
self.backgroundColor = UIColor.colorYellow() // Changes immediately to yellow
If I animate alpha, this animates from 1 to 0 over one second as expected:
self.alpha = 0 // animates
How do I animate a background color change in this situation?
Implementing drawRect blocks backgroundColor animation, but no answer is provided yet.
Maybe this is why you can't combine drawRect and animateWithDuration, but I don't understand it much.
I guess I need to make a separate view--should this go in the storyboard in the same view controller? programmatically created?
Sorry, I'm really new to iOS and Swift.
It is indeed not working when I try it, I had a related question where putting the layoutIfNeeded() method inside the animation worked and made the view smoothly animating (move button towards target using constraints, no reaction?). But in this case, with the backgroundColor, it does not work. If someone knows the answer I will be interested to know.
But if you need a solution right now, you could create a UIView (programmatically or via the storyboard) that is used only as a container. Then you add 2 views inside : one on top, and one below, with the same frame as the container. And you only change the alpha of the top view, which let the user see the view behind :
class MyView : UIView {
var top : UIView!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.blueColor()
top = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
top.backgroundColor = UIColor.yellowColor()
self.addSubview(top)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let sub = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
sub.backgroundColor = UIColor.purpleColor()
self.sendSubviewToBack(sub)
UIView.animateWithDuration(1, animations: { () -> Void in
self.top.alpha = 0
}) { (success) -> Void in
println("anim finished")
}
}
}
The answer is that you cannot animate backgroundColor of a view that implements drawRect. I do not see docs for this anywhere (please comment if you know of one).
You can't animate it with animateWithDuration, nor with Core Animation.
This thread has the best explanation I've found yet:
When you implement -drawRect:, the background color of your view is then drawn into the associated CALayer, rather than just being set on the CALayer as a style property... thus prevents you from getting a contents crossfade
The solution, as #Paul points out, is to add another view above, behind, or wherever, and animate that. This animates just fine.
Would love a good understanding of why it is this way and why it silently swallows the animation instead of hollering.
Not sure if this will work for you, but to animate the background color of a UIView I add this to a UIView extension:
extension UIView {
/// Pulsates the color of the view background to white.
///
/// Set the number of times the animation should repeat, or pass
/// in `Float.greatestFiniteMagnitude` to pulsate endlessly.
/// For endless animations, you need to manually remove the animation.
///
/// - Parameter count: The number of times to repeat the animation.
///
func pulsate(withRepeatCount count: Float = 1) {
let pulseAnimation = CABasicAnimation(keyPath: "backgroundColor")
pulseAnimation.fromValue = <#source UIColor#>.cgColor
pulseAnimation.toValue = <#target UIColor#>.cgcolor
pulseAnimation.duration = 0.4
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = count
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.layer.add(pulseAnimation, forKey: "Pulsate")
CATransaction.commit()
}
}
When pasting this in to a source file in Xcode, replace the placeholders with your two desired colors. Or you can replace the entire lines with something like these values:
pulseAnimation.fromValue = backgroundColor?.cgColor
pulseAnimation.toValue = UIColor.white.cgColor

Resources