UIScrollView in tvOS - swift

I have a view hierarchy similar to the one in the image below (blue is the visible part of the scene):
So I have a UIScrollView with a lot of elements, out of which I am only showing the two button since they are relevant to the question. The first button is visible when the app is run, whereas the other one is positioned outside of the initially visible area. The first button is also the preferredFocusedView.
Now I am changing focus between the two buttons using a UIFocusGuide, and this works (checked it in didUpdateFocusInContext:). However, my scroll view does not scroll down when Button2 gets focused.
The scroll view is pinned to superview and I give it an appropriate content size in viewDidLoad of my view controller.
Any ideas how to get the scroll view to scroll?

Take a look at UIScrollView.panGestureRecognizer.allowedTouchTypes. It is an array of NSNumber with values based on UITouchType or UITouch.TouchType (depending on language version). By default allowedTouchTypes contains 2 values - direct and stylus. It means that your UIScrollView instance will not response to signals from remote control. Add the following line to fix it:
Swift 4
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouchType.indirect.rawValue)]
Swift 4.2 & 5
self.scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)]
Also, don't forget to set a correct contentSize for UIScrollView:
self.scrollView.contentSize = CGSize(width: 1920.0, height: 2000.0)

Finally I solved this by setting scrollView.contentSize to the appropriate size in viewDidLoad.

You need to add pan gesture recognizer. I learned from here: http://www.theappguruz.com/blog/gesture-recognizer-using-swift. I added more code to make it not scrolling strangely, e.g. in horizontal direction.
var currentY : CGFloat = 0 //this saves current Y position
func initializeGestureRecognizer()
{
//For PanGesture Recoginzation
let panGesture: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("recognizePanGesture:"))
self.scrollView.addGestureRecognizer(panGesture)
}
func recognizePanGesture(sender: UIPanGestureRecognizer)
{
let translate = sender.translationInView(self.view)
var newY = sender.view!.center.y + translate.y
if(newY >= self.view.frame.height - 20) {
newY = sender.view!.center.y //make it not scrolling downwards at the very beginning
}
else if( newY <= 0){
newY = currentY //make it scrolling not too much upwards
}
sender.view!.center = CGPoint(x:sender.view!.center.x,
y:newY)
currentY = newY
sender.setTranslation(CGPointZero, inView: self.view)
}

Related

How can I implement this UICollectionView cell animation?

I've been wanting to implement this nice little UICollectionViewCell animation shown below that was on Dribble.
Do you think it's possible?
I looked around for any guides to get a head start on this but found nothing quite similar.
I have the idea that a custom flow layout is the way to go here. Would it be that I will have to make snapshots of each visible cell, add pan gestures to each cell and based on the movement detected through the gesture recogniser, capture visible cells and animate the snapshot images? Would appreciate any help to understand how I could implement this.
Thank you.
This is a pretty interesting challenge.
Instead of doing a custom layout, I would override scrollViewDidScroll, store the offset every time it's called, compare it with the last stored offset in order to get the velocity, and based off of that, apply a transform to all visibleCells in your collection view.
var lastOffsetX: CGFloat?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
defer { lastOffsetX = scrollView.contentOffset.x }
guard let lastOffsetX = lastOffsetX else { return }
// You'll have to evaluate how large velocity gets to avoid the cells
// from stretching too much
let maxVelocity: CGFloat = 60
let maxStretch: CGFloat = 10
let velocity = min(scrollView.contentOffset.x - lastOffsetX, maxVelocity)
let stretch = velocity / maxVelocity * maxStretch
var cumulativeStretch: CGFloat = 0
collectionView.visibleCells.forEach { cell in
cumulativeStretch += stretch
cell.transform = CGAffineTransform(translateX: cumulativeStretch, y: 0)
}
}
I would start with something like this, and make lastOffsetX = nil when the scroll view stops scrolling (this exercise is left to the reader).
It will probably require some tweaking.

iOS Swift - Position subview based on button frame (button is inside Vertical Stack View)

Update
After adding convert method between rect and main View, the Y position is ok, but X coordinate is shifted to the right outside of the main view:
Dropdown view(subview) is off main view
Below is button frame before and after convert method. Main view is 414 x 896. Dropdown menu somehow shifts to the right as on attached image.
button frame in stackView btnRect: (120.66666666666666, 0.0, 293.3333333333333, 30.0)
button frame in main view: cvtRect (241.33333333333331, 190.0, 293.3333333333333, 30.0)
view: Optional(>)
Goal. I want to make a dropdown list by showing UIView with dropdown options below a button. Button is inside of Vertical Stack View. I add a TableView with dropdown options to this dropdown UIView.I want to click a button and have this UIView with TableView inside to show just below the button. Basically following this tutorial https://www.youtube.com/watch?v=D3DCPaEE4hQ with the exception that my button is inside of Vertical Stack View.
Issue. UIView and TableView inside UIView show up ok when button is clicked. The issue is dropdown UIView's location that is always the same origin X=120, Y=0.
This is how I try to do it:
I have Vertical Stack with 4 rows
In 4th row I have label(width=120 almost same as X coordinate above) and a button that triggers UIView to show
I am using button to show dropdown list(basically UIView) that should appear just below button when the button is tapped, but it always appears at origin x=120 Y=0 , basically pinned to top of the right column in Vertical Stack View. Vertical Stack View has 1st column with labels, and second column with different controls like buttons etc.
func addTransparentView(frames: CGRect)
{
let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
transparentView.frame = window?.frame ?? self.view.frame
//some of the stuff I tried to at least centre dropdownUIView
//transparentView.center.x = window?.center.x ?? self.view.center.x
//transparentView.center.y = window?.center.y ?? self.view.center.y
self.view.addSubview(transparentView)
tvPriority.frame = CGRect(x: frames.origin.x, y: frames.origin.y + frames.width, width: frames.width, height: 0)
//some of the stuff I tried to at least centre UIView
//tvPriority.center = verticalStackView.convert(verticalStackView.center, from:tvPriority)
//tvPriority.center.x = view.center.x
//tvPriority.center.y = view.center.y
self.view.addSubview(tvPriority)
tvPriority.layer.cornerRadius = 5
transparentView.backgroundColor = UIColor.black.withAlphaComponent(0.9)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(removeTransparentView))
transparentView.addGestureRecognizer(tapGesture)
transparentView.alpha = 0
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: UIView.AnimationOptions.curveEaseInOut, animations: {self.transparentView.alpha = 0.5
self.tvPriority.frame = CGRect(x: frames.origin.x, y: frames.origin.y + frames.height, width: frames.width, height: 193)
}, completion: nil)
}
To successfully make dropdown list I need UIView to show up just below buttons frame(X, Y, width, height). But although button is in the 4th row which should be position with much higher Y value, buttons frame is always at X=120, Y=0, so my UIView is always pinned to this location way above button that is supposed to simulate dropdown.
Questions
1. What am I missing with positioning of the dropdown UIView? Why is buttons position Y=0 when the button is in 4th row of Vertical Stack View, with obviously much higher Y position? I also tried to simply centre this dropdown in the centre of screen but that also does not work.
2. I transitioned to iOS development from the world of web development, and I used dropdown a lot in my career. Should I just use Picker View instead? Or alert? What is the most common and most standard way of offering list of mutually exclusive options to user in Swift app?
Thanks a lot
Your button is a subview of the stackView, so its frame is relative to the frame of the stackView.
To get its frame (rect) in the view's coordinate space, you'll want to use .convert. Assign this action to one of your buttons in the stackView:
EDIT Fixed the code example... I had not checked it before posting.
class ConvertViewController: UIViewController {
#IBOutlet var dropDownView: UIView!
#IBAction func didTap(_ sender: Any) {
guard let btn = sender as? UIButton else {
fatalError("Sender is not a button!")
}
guard let sv = btn.superview as? UIStackView else {
fatalError("Sender is not in a stackView!")
}
let btnRect = btn.frame
let cvtRect = sv.convert(btn.frame, to: view)
print("button frame in stackView:", btnRect)
print("button frame in main view:", cvtRect)
let dropDownRect = dropDownView.bounds
let cvtCenterX = cvtRect.origin.x + (cvtRect.size.width / 2.0)
let viewX = cvtCenterX - (dropDownRect.width / 2.0)
let newOrigin = CGPoint(x: viewX, y: cvtRect.minY + cvtRect.height)
dropDownView.frame.origin = newOrigin
}
}
If you look at the output in the debug console, you should see something like this:
button frame in stackView: (0.0, 114.0, 46.0, 30.0)
button frame in main view: (164.5, 489.5, 46.0, 30.0)
As you can see, the rect (frame) of my 4th button in my stackView has an origin of 0.0, 114.0, but after converting it to my view coordinate space, the rect's origin is 164.5, 489.5.
You can now position your "dropdown list" relative to the converted rect.
As a side note, you may want to look at UIPopoverPresentationController.

Swift method that is called after autolayout is finalized

I have a method that draws pins by adding an ImageView on top of a slider when a button is pressed. Here is the code:
var loopStartImageView : UIImageView = UIImageView()
func addLoopDrawing(at time: CMTime, for loop: Int) {
let imgHeight : CGFloat = 30
let imgWidth: CGFloat = 30
var pinImg : UIImage = UIImage(named: "pin2x")!
let inset = slider.frame.origin.x
let width = slider.bounds.width
let xPos : CGFloat = CGFloat((Float(time.seconds) / slider.maximumValue)) * width + inset - (imgWidth / 2)
var yPos : CGFloat = slider.frame.origin.y - (slider.frame.height / 2) - 3
let imgV = UIImageView(frame: CGRect(x: xPos, y: yPos, width: imgWidth, height: imgHeight))
imgV.image = pinImg
loopStartImageView = imgV
view.addSubview(loopStartImageView)
view.sendSubviewToBack(loopStartImageView)
}
The drawing is correct when I don't use autolayout to set the position of the slider, but once I do it shows up below the actual positioning.
This method is called in viewDidLoad. My guess is that for some reason the auto layout positioning is not set when viewDidLoad is called.
I was wondering if there is a method that is called once auto layout is fully adjusted?
//
This is what it should look like:
But this is how the view loads (even when calling the addLoopDrawing function inside the viewDidLayoutSubviews)
I was wondering if there is a method that is called once auto layout is fully adjusted?
That would be viewDidLayoutSubviews. That is a very good time to do things that depend upon things having their correct frame, but beware: it can be called many times. The usual thing, therefore, is to implement it along with a Bool property to check that this is the first time.
On the other hand, it might be better not to ask an x-y question like this. The idea of an image view on top of a slider seems wrong. It might be better to describe what you are really trying to do.

Getting the View's center when initialing a variable

I'm very new to Swift and trying to make a simple application to learn.
I'm trying to setup on screen a UISlider and set it in the middle of the screen, the problem is that the view is probably isn't ready yet.
I tried to do something like this:
> private let bullsEyeSlider: UISlider = {
> let slider = UISlider(frame: CGRect(x: self.view.frame.midX,y:100, width: 500, height: 20))
> slider.minimumValue = 0
> slider.maximumValue = 100
> return slider
> }()
to get the center of the view (the x center at least) but of course it can't work.
I'm trying to think of something else to do but other than just setting up the slider after the view is initialised I can't think of anything else.
Tried to search the web but with no luck.
In viewDidLayoutSubviews method of the view controller add:
bullsEyeSlider.center = view.center

Swift: "Freeze" bouncing in a scrollView

I am trying to work a little bit with scrollViews. So I have a ScrollView on my Controller with bouncing enabled. So now then I scroll the view bounces at the end of the page like I want to.
But now I want to freeze my bouncing. I try to explain it a little bit more:
I scroll up in my ScrollView. The end of the page begins. So now the bouncing begins and its scrolling a little bit up. Then I undrag the scrollView the view bounces back. But I want the scrollView to stay in this position (because I push the view away and it looks weird when its bouncing while I push it).
Things I tried:
1.Set the frame on the scroll view to the current bouncing position. This just edits the frame but still bounces the scrollView.
v2.view.frame = CGRect(x: CGFloat(0), y: CGFloat(CURRENTBOUNCING), width: width, height: height)
2.Turn bounces off. This just ends the current bounce and after you can't bounce anymore. So if I don't want it to bounce back this doesn't work.
scrollView.bounces = false
3.Set contentOffset manually. Changes nothing for me even if I write it after initializing
scrollView.contentOffset.y -= 100 // Or something like this
4.Change the contentSize. This changes nothing I think because of the constraints
scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height - 200)
5.Turn always bouncing vertically off. Same like 2.
scrollView.alwaysBounceVertical = false
6.Paging. I tried this because now I page manually. But then I miss the bouncing into nothing before paging. With paging you can see the next page while scrolling behind the end. But the next page should be loaded after dragging.
Hope someone can help me :)
EDIT
7.Taking a picture of my current scrollview to display it on my screen over the scrollview. This takes an image with an delay and not at the right moment ( so the bounces is at the half then capturing or already done.
let renderer = UIGraphicsImageRenderer(size: scrollView.bounds.size)
pufferImage.image = renderer.image { ctx in
scrollView.drawHierarchy(in: scrollView.bounds, afterScreenUpdates: true) }
Ran into the same problem. The solve is to record the content offset and change the vertical offset of the scrollview to that value while simultaneously turning scrolling off.
private var dismissTriggerd = false
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentOffset" {
if scrollView.contentOffset.y < -70 && dismissTriggerd == false {
scrollViewTop.constant = scrollView.contentOffset.y * -1
scrollView.isScrollEnabled = false
dismissTriggerd = true
dismiss(animated: true)
}
}
}
deinit {
scrollView.removeObserver(self, forKeyPath: "contentOffset")
}
I had a similar use case, the key was to switch the contentOffset into contentInset this negated the effect of the bounce back
maybe not a 100% complete solution, but i leave it here for inspiration
case UIGestureRecognizerStateEnded:
self.interactor.hasStarted = NO;
if (self.interactor.shouldFinish) {
UIEdgeInsets ei = self.scrollView.contentInset;
ei.top -= self.scrollView.contentOffset.y;
self.scrollView.contentInset = ei;
[self.interactor finishInteractiveTransition];
} else {
[self.interactor cancelInteractiveTransition];
}
break;
default: