Animating a mask alongside an expanding CollectionViewCell - swift

I am working on a transition during which a collectionView cell expands to reveal new elements.
In order to prevent the new elements from animating with the cell, I removed them from the animated view.
Here is an image of the view hierarchy
After I remove the elements from the animated view, they are also not being clipped by the cell anymore, revealing them before the cell has expanded.
Right now I can decide between the elements animating with the cell or the elements appearing before they should.
Here is a video of the current state of the animation
You can see the new elements appearing on the cell to the left of the selected one.
Below is the code for my animated transitioning:
let destination = transitionContext.viewController(forKey: .to)
let containerView = transitionContext.containerView
containerView.addSubview(destination.view)
// Initial state
let widthConstraint = destination.header.widthAnchor.constraint(equalToConstant: 500)
let heightConstraint = destination.header.heightAnchor.constraint(equalToConstant: 601)
NSLayoutConstraint.activate([widthConstraint, heightConstraint])
let transform = CATransform3DMakeTranslation(cellFrame.origin.x, cellFrame.origin.y, 0.0)
destination.topView.layer.transform = transform
destination.view.layer.zPosition = 999
containerView.layoutIfNeeded()
let animator = UIViewPropertyAnimator(duration: 4, dampingRatio: 10) {
// Final state
NSLayoutConstraint.deactivate([widthConstraint, heightConstraint])
destination.topView.layer.transform = CATransform3DIdentity
destination.view.layoutIfNeeded()
}
I was thinking about masking the new elements based on the frame of the expanding cell but am not sure how I could make that work.

An alternative solution you could do is to transition the labelingView to show only when it should be present. For example, only show your labelingView once you finish your animation transition.
Some psuedo code..
labelingView.alpha = 0
UIView.animate(withDuration: 1, animations: {
// perform transitions
}) { _ in
labelingView.alpha = 1
}

I solved it an it turned out to be easier than expected.
I simply created a mask based on the initial collectionViewCell and animated it to fullscreen alongside the expanding cell.
let mask = UIView()
mask.frame.origin = CGPoint(x: cellFrame.origin.x, y: cellFrame.origin.y)
mask.frame.size = CGSize(width: 500, height: 601)
mask.backgroundColor = .white
mask.alpha = 1
destination.labelingView.mask = mask
// Final mask state
mask.frame.origin = CGPoint(x: 0.0, y: 0.0)
mask.frame.size = CGSize(width: 1366, height: 1024)

Related

Change content size UIScrollView dynamically

I have a scroll view, when the view is first loaded, the size is set dynamically, but when I click on the button, the internal size of my elements changes and I need to change the internal size of the scroll, but it does not change. Someone knows how to fix it?
DispatchQueue.main.async {
var contentRect = CGRect()
for view in self.scrollView.subviews {
contentRect = contentRect.union(view.frame)
self.scrollView.contentSize = contentRect.size
}
}
If you really don't want to use auto-layout / constraints, you can call this function each time you add (or remove) a subview from the scroll view, or after you've changed the size(s) of the subview(s):
func updateContentSize() -> Void {
// this will get the right-edge of the right-most subview
let width = scrollView.subviews.map {$0.frame.maxX}.max() ?? 0.0
// this will get the bottom-edge of the bottom-most subview
let height = scrollView.subviews.map {$0.frame.maxY}.max() ?? 0.0
// set the contentSize
scrollView.contentSize = CGSize(width: width, height: height)
}
This solution is for auto-layout/constraints.
You need a reference constraint to manipulate the height of the inner container view of the scrollview.
private var _constraintInnerContainerScroll:NSLayoutConstraint?
You need to set the initial height of the inner container view, suppose 700.0
private let _containerViewHeightFixed : CGFloat = 700.0
then you need to save the reference
_constraintInnerContainerScroll = _containerView.heightAnchor.constraint(equalToConstant: _containerViewHeightFixed)
_constraintInnerContainerScroll?.isActive = true
You initial view is setup and ready, now suppose you add 2 more subview of height 100.0 each, now your new inner container view height should be 700.0+200.0 = 900.0
if let const1 = _constraintInnerContainerScroll{
const1.constant = _containerViewHeightFixed + 200.0
UIView.animate(withDuration: 0.5) {
self?._containerView.layoutIfNeeded()
}else{
print("constraint reference not saved")
}
let me know if this works for you, or if this can be improved.

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.

How to capture image of entire view's contents when bigger than screen

I need my app to render everything that a view controller has the potential to display (including off-screen content) except for the top and bottom navigation bars.
The first image, below, shows the view controller at runtime. The action menu triggers the following code which is adapted the code sample from the answer given here :
#IBAction func actionMenu(_ sender: Any) {
let activityItems = [generateImageOfTableView(tblview: tourneyEntrants)]
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
activityController.popoverPresentationController?.sourceView = self.view
activityController.popoverPresentationController?.sourceRect = self.view.frame
self.present(activityController, animated: true, completion: nil)
}
func generateImageOfTableView(tblview: UITableView) -> UIImage {
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(tblview.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = tblview.contentOffset;
let savedFrame = tblview.frame;
let savedBackgroundColor = tblview.backgroundColor
// reset offset to top left point
tblview.contentOffset = CGPoint(x: 0, y: 0);
// set frame to content size
tblview.frame = CGRect(x: 0, y: 0, width: tblview.contentSize.width, height: tblview.contentSize.height);
// remove background
tblview.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: tblview.contentSize.width, height: tblview.contentSize.height))
// save superview
let tempSuperView = tblview.superview
// remove scrollView from old superview
tblview.removeFromSuperview()
// and add to tempView
tempView.addSubview(tblview)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(tblview)
// restore saved settings
tblview.contentOffset = savedContentOffset;
tblview.frame = savedFrame;
tblview.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
}
The second image, below, shows the image captured from this code.
There are two problems with it.
The first is that it is ignoring the text field and label above the table. I know that the code doesn't look for this, so I am looking for some guidance on how to capture the superview's contents (minus the navigation bars).
Second, the table view contains 18 columns of numbers but these aren't captured. So, the code copes with the height of the table being beyond the screen but not with the width. I've looked at whether auto layout maybe causing this, but cannot see anything obvious.

How to center a CGImage on a CALayer?

I was trying to set a UIImage's CGImage as a layer's content and then add the layer to a view's layer.
It's should be five stars at the center of the yellow view. That's what I want it to be.
But it seems the center of the stars is aligned with the origin of the view.
What should I do to rectify it?
func putOnStars() {
let rect = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
rect.backgroundColor = .yellow
view.addSubview(rect)
let baseLayer = CALayer()
baseLayer.contents = UIImage(named: "stars")?.cgImage
baseLayer.contentsGravity = kCAGravityCenter
rect.layer.addSublayer(baseLayer)
}
Here is the stars image for you in case of you want to test.
baseLayer doesn't have a defined frame so baseLayer.contentsGravity = kCAGravityCenter will work fine but it'll still be on the potision (0, 0).
There are two possible solutions:
1 : Make a frame of baseLayer that is identical to rect. Implement this code:
baseLayer.frame = rect.frame
2 : Set the position of baseLayer to the center of rect. Implement this code:
baseLayer.position = rect.center
To place the stars image in the centre of the CALayer, give the frame of the layer, i.e.
let baseLayer = CALayer()
baseLayer.frame = rect.bounds //This Line
baseLayer.contents = UIImage(named: "stars")?.cgImage
baseLayer.contentsGravity = kCAGravityCenter
rect.layer.addSublayer(baseLayer)
For any kind of CALayer, you need to define its frame explicitly.

UIScrollView with "Circular" scrolling

I am trying to make "Circular" scrolling in my UIScrollView, but unsuccessful.
What I want to do:
if uiscrollview reaches end, it should move to start
if uiscrollview at start and moving back, it should move to end
Appending scrollview isn't good way in my situation (other methods should get "page id")
Have you any ideas?
I've implemented this method, but it requires paging enabled. Lets assume you have five elements A,B,C,D and E. When you set up your view, you add the last element to the beginning and the first element to the end, and adjust the content offset to view the first element, like this E,[A],B,C,D,E,A. In the UIScrollViewDelegate, check if the user reach any of the ends, and move the offset without animation to the other end.
Imagine the [ ] indicates the view being shown:
E,A,B,C,[D],E,A
User swipes right
E,A,B,C,D,[E],A
User swipes right
E,A,B,C,D,E,[A]
Then, automatically set the content offset to the second element
E,[A],B,C,D,E,A
This way the user can swipe both ways creating the illusion of an infinite scroll.
E,A,[B],C,D,E,A
Update
I've uploaded a complete implementation of this algorithm. It's a very complicated class, because it also has on-click selection, infinite circular scroll and cell reuse. You can use the code as is, modify it or extract the code that you need. The most interesting code is in the class TCHorizontalSelectorView.
Link to the file
Enjoy it!
Update 2
UICollectionView is now the recommended way to achieve this and it can be used to obtain the very same behavior. This tutorial describes in details how to achieve it.
Apple has a Street Scroller demo that appears to have exactly what you want.
There's also a video from WWDC 2011 that demos their demo. ;) They cover infinite scrolling first in the video.
Try to use following code..
Sample code..
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
if (scrollView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image n(last image).
// reposition offset to show image 10 that is on the right in the scroll view
[scrollView scrollRectToVisible:CGRectMake(4000,0,320,480) animated:NO];// you can define your `contensize` for scrollview
}
else if (scrollView.contentOffset.x == 4320) {
// user is scrolling to the right from image n(last image) to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[scrollView scrollRectToVisible:CGRectMake(320,0,320,480) animated:NO];
}
}
Hope, this will help you...
With reference from #redent84, find the code logic.
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
scrollView.contentSize = CGSize(width: scrollView.frame.width * CGFloat(imagesArray.count), height: scrollView.frame.height)
scrollView.isPagingEnabled = true
// imagesArray.insert(imagesArray.last as? UIImage, at: 0)
// imagesArray.insert(imagesArray.first as? UIImage, at: imagesArray.count - 1)
var testArr = ["A", "B", "C", "D"]
let firstItem = testArr.first
let lastItem = testArr.last
testArr.insert(lastItem as! String, at: 0)
testArr.append(firstItem as! String)
print(testArr)
for i in 0..<imagesArray.count{
let imageview = UIImageView()
imageview.image = imagesArray[i]
imageview.contentMode = .scaleAspectFit
imageview.clipsToBounds = true
let xPosition = self.scrollView.frame.width * CGFloat(i)
imageview.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: self.scrollView.frame.height)
print(imageview)
scrollView.addSubview(imageview)
print("Imageview frames of i \(i) is \(imageview.frame)")
}
newStartScrolling()
}
func newStartScrolling()
{
ViewController.i += 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
print("X is \(x) and i is \(ViewController.i)")
if ViewController.i == imagesArray.count - 1 {
ViewController.i = 0
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
//scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
print("X (rotate) is \(x) and i is \(ViewController.i)")
scrollView.contentOffset.x = x
self.newStartScrolling()
}
}
func backScrolling() {
ViewController.i -= 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
print("X is \(x) and i is \(ViewController.i)")
if ViewController.i <= 0 {
ViewController.i = imagesArray.count - 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
print("X (rotate) is \(x) and i is \(ViewController.i)")
self.backScrolling()
//scrollView.contentOffset.x = x
return
}
}