Programmatic scrolling Apple TV Flicking cells - swift

I'm creating an UICollectionView that moves automatically, without user interaction, using:
private func startWaterfallAnimation() {
var timer = Timer.init()
timer = Timer.scheduledTimer(withTimeInterval: 0.06, repeats: true) { (_) in
if !self.isPaused{
UIView.animate(withDuration: 0.06, delay: 0, options: [.curveLinear], animations: {
self.setContentOffset(CGPoint(x: self.contentOffset.x, y: self.contentOffset.y - CGFloat(self.speed)), animated: false)
}, completion: nil)
} else {
timer.invalidate()
}
}
}
Unfortunately, this is the behaviour: https://giphy.com/gifs/1xV9RiPlOoSTugIx1s, cells flick when moving.
My question is: How can I programmatically scroll in a smooth way at Apple TV, without the flick? Maybe another approach?

This is not a code problem, I've tried many approaches. It has to do with the TV resolution, pixels are bigger and when trying to scroll something programmatically in a smooth way, if you do it slow it will flick in most TVs.

Related

View animating from the incorrect position

I am trying to animate a cover view in my code. Essentially the cover slides up (closes) from off-screen to on-screen, and after the user presses a button the cover slides down (opens up) all the way off-screen.
My code to slide the cover view up the screen and into view is working perfectly. However, the code to slide the cover view down the screen and out of view is not working! The starting position for the cover sliding down is what seems to be incorrect. The cover slides down as expected, but starts far too high, such that the animation ends where it should start.
I've got a screen recording of the error in action (obviously slowed the animation down so you can see what I mean by the error). The cover sliding down should be sliding down from its original position where it slid up to.
Link to recording.
Can anyone spot where I'm going wrong in my code?
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
func hideScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseIn,
animations:
{ self.ScoresCoverView.frame.origin.y += self.verticalDistance},
completion:
{ finished in
print("Score cover opened.")
})
}
EDIT
hey #Nickolans thanks for your help on this issue. So... I have a bizarre update for you on this query. I implemented your code, and it works if I have a pre-programmed button which fires the showScoreCover and hideScoresCover. Turns out even my original code works, but again, only if I have a pre-programmed button which fires the show/hide functions.
However, I need to fire the function using a button where I programmatically reconfigure what the button does. How I've implemented this is through .addTarget and a number of selectors, and those selectors will differ based on where in the game the users are.
Anyway, this is what my code looks like:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Originally I only used one .addTarget and included the hideScoresCover() in the startTeamB function. This was resulting in the original issue where the cover doesn't hide correctly (starts too high on the screen). So I split it up into the above two .addTargets, and that also results in the same issue. HOWEVER, if I comment out the entire .addTarget line above related to the startTeamB selector and leave the selector related to hideScoresCover, then the cover is shown and is hidden absolutely as I want to to be, the animation is perfect. This means the underlying code in hideScoresCover() is working fine, but that the issue is arising somehow in how hideScoresCover is being run when the continue button is pressed.
Do you have any idea why this would be? somehow that continueButton target is firing incorrectly whenever there are two addTargets, or when there is only one target but hideScoresCover() is included in startTeamB().
I'm stumped as to why this would be the case. startTeamB() is not a complex function at all, so shouldn't be interfering with hideScoresCover(). So this leads me only to conclude it must be the way the continueButton fires hideScoresCover() when its either a separate target in addition to startTeamB being a target, or when hideScoresCover() is included in startTeamB itself. Unusual that it does work correctly when the only .addTarget is a selector calling hidesScoresCover?
#objc func startTeamB() {
self.UpdateTeam() //Just changes the team name
self.TeamBTimer() //Starts a timer to countdown teamB's remaining time
self.TeamAIndicator.isHidden = true //Just hides teamA image
self.TeamBIndicator.isHidden = false //Just shows teamB image
print("Hintify: ","-----Start Team B.")
}
I took it into Xcode and set it up myself. I believe the issue may be how you're changing your Y value, instead of changing the origin change the view layers y position.
The following code works so you can just take it! Make sure to change the frame to whatever you need it to be when you first start.
class ViewController: UIViewController {
//Bool to keep track if view is open
var isOpen = false
var verticalDistance: CGFloat = 0
var ScoresCoverView = UIView()
var button = UIButton()
override func viewDidLoad() {
//Setup Score view
ScoresCoverView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
ScoresCoverView.backgroundColor = .blue
view.addSubview(ScoresCoverView)
//Set vertical distance
verticalDistance = ScoresCoverView.frame.height
//TEST button to test view animations
button.frame = CGRect(x: 200, y: 500, width: 100, height: 100)
button.backgroundColor = .purple
button.addTarget(self, action: #selector(hasBeenFired), for: .touchUpInside)
self.view.addSubview(button)
}
//When button pressed
#objc func hasBeenFired() {
if isOpen {
self.hideScoresCover()
self.isOpen = false
} else {
self.showScoresCover()
self.isOpen = true
}
}
//Show
func showScoresCover() {
self.ScoresCoverView.isHidden = false
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseOut, animations: {
self.ScoresCoverView.layer.position.y -= self.verticalDistance
}) { (finished) in
print("Score cover closed.")
}
}
//Hide
func hideScoresCover() {
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseIn, animations: {
self.ScoresCoverView.layer.position.y += self.verticalDistance
}) { (finished) in
self.ScoresCoverView.isHidden = true
print("Score cover Opened.")
}
}
}
EDIT
If you'd like to change the functionality of the button, make sure to remove the previous target before adding a new one.
For example, if you want to switch from show/hide to teamB remove show/hide and add teamB-- and vice versa.
When you first start, add your initial target:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
Switch to startTeamB:
self.continueButton.removeTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Switch to hideScoreCover:
self.continueButton.removeTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
I just want to say one thing, maybe the += is not necessary here.
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
should be
`self.ScoresCoverView.frame.origin.y = verticalDistance`

How can i interact with views while they keep animating in UIKit (swift 4)?

I have a set of shapes (UIViews), that can be moved on the screen using a UIPanGestureRecognizer, and that are supposed to wiggle continuously and forever after the user triggers the animation. But I want them to keep wiggling, even while the user picks one up and moves it.
What is practically happening is that when the user picks up a shape, it stops animating, and when he puts it back down, it doesn't even restart the animation.
Here's some code for the context:
func wiggle() {
UIView.animate(withDuration: 0.15, options: [.repeat, .autoreverse, .allowUserInteraction], animation: {
self.view.subviews.forEach { $0.transform = CGAffineTransform(rotationAngle: CGFloat.pi/8) })
}
func pickUpShape() {
[.....]
shape.transform = CGAffineTransform(scaleX: 2, scaleY: 2)
shape.layer.shadowOpacity = 0.7
}
func moveShape() {
//apply UIPanGestureRecognizer
}
func placeShape() {
[.....]
shape.transform = .identity
shape.layer.shadowOpacity = 0
}
What I want it to do is, if the user triggers the wiggle action, and picks a shape up and moves it, i want the shape to continue wiggling under the user's finger, and after he places it down.

Swift contentOffset scroll

I am developing a small application for tvOS where I need to show a UITextView.
Unfortunately sometimes this text can't fit inside the screen, that's why I need a way to scroll to the bottom of it programmatically.
I've tried using the following code:
self.setContentOffset(offset, animated: true)
It's actually working as intended, except for the fact that I need to handle the animation speed, so that the user can actually read; at the moment it's too fast.
This is what I tried to do, but with little success:
let offset = CGPoint(x: 0, y: contentSize.height - bounds.size.height)
UIView.animate(withDuration: 4, animations: {
self.setContentOffset(offset, animated: false)
})
I need a way to keep the text in place and scroll it to show the missing lines.
I am using a UITextView inside an UIScrollView.
Thanks.
If anyone is looking for a solution, this is what I ended up using:
var textTopDistance = 100
//calculate height of the text not being displayed
textNotVisibleHeight = descriptionLabel.contentSize.height - scrollView.bounds.size.height
func scrollText() {
//start new thread
DispatchQueue.main.async() {
//animation
UIView.animate(withDuration: 6, delay: 2, options: UIViewAnimationOptions.curveLinear, animations: {
//set scrollView offset to move the text
self.scrollView.contentOffset.y = CGFloat(self.textNotVisibleHeight - self.textTopDistance)
}, completion: nil)
}
}
It's not perfect I know, but at least it's working as intended.

Repeating iCarousel's scrolling

I'm using iCarousel in Swift 3 in Xcode. I'm trying to make it work like a sliding banner. I have this function:
func animate() {
DispatchQueue.global(qos: .userInteractive).async {
self.carousel.scrollToItem(at: 0, animated: false)
while(true){
sleep(3)
self.carousel.scroll(byNumberOfItems: 1, duration: 0.3)
}
}
}
I call it in viewDidLoad. It works but I think it's not as good as it could be. After I switch to another view, scrolling goes on and on. What would be the best approach to make it scroll only, when I'm seeing the view, that the carousel is in?
What solved the problem:
var timer: Timer!
self.timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(self.handleTimer), userInfo: nil, repeats: true)
func handleTimer(){
self.carousel.scroll(byNumberOfItems: 1, duration: 0.3)
}

Image is cut when device it's rotated using a slide show with timer

I've developed a slide show using a UIScrollView and I have implemente a NSTimer to change the images automatic in a lapse of time in the following way :
override func viewDidLoad() {
...
isLandscape = UIApplication.sharedApplication().statusBarOrientation.isLandscape
NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("updateScrollView"), userInfo: nil, repeats: true)
}
The method to call in change :
func updateScrollView(){
var offsetX = scrollView.contentOffset.x
if (self.isLandscape!){
scrollView.setContentOffset(CGPoint(x: offsetX + 1024, y: 0), animated: true)
}
else{
scrollView.setContentOffset(CGPoint(x: offsetX + 768, y: 0), animated: true)
}
}
I create a variable named isLandscape to know when the device is in portrait or landscape and in base of this know how much to sum to the contentOffset.
But for some reason if I rotate the device in the precise moment that the timer it's called the image don't advance correctly to the next one, we can see one part of the previous and one part of the next, of course if I move it wiht my finger all it back to work.
How can I avoid the rotation if the timer it's called in this moment?
Any advice?
Stop (invalidate) the timer when rotation begins, and start it again (create a new one) when rotation has ended. Your view controller gets events that tell you when rotation will start and when it has ended.
(Another possibility: simply detect that rotation has ended and call updateScrollView then and there, to force the next slide in case we are in the "stuck" position. But I think my first suggestion is probably better.)