viewWillAppear for subviews - iphone

I have UIScrollView with multiple UIVIew subviews. I would like to update the data that is displayed by each UIView when they appear in the visible portion of the UIScrollView.
What is the callback that gets triggered? I tried viewWillAppear, but it does not seem to get called.
Thanks. :)

You have to do the calculation yourself. Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.

Swift 3 solution
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = greenView.frame
let container = CGRect(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y, width: scrollView.frame.size.width, height: scrollView.frame.size.height)
// We may have received messages while this tableview is offscreen
if (viewFrame.intersects(container)) {
// Do work here
print("view is visible")
}
else{
print("nope view is not on the screen")
}
}

Above answers are correct if your scrollview is not in the zoomed in state. In case if your scrollview can zoom above calculation won't work as you need to consider zoom too
here is the code
CGRect visibleRect;
visibleRect.origin = self.mapScrollView.contentOffset;
visibleRect.size = self.mapScrollView.bounds.size;
float theScale = 1.0 / self.mapScrollView.zoomScale;
visibleRect.origin.x *= theScale;
visibleRect.origin.y *= theScale;
visibleRect.size.width *= theScale;
visibleRect.size.height *= theScale;
if(CGRectIntersectsRect(visibleRect, btnPin.frame)){
...
}

A slight refinement. I wanted to know the amount of the view that was displayed in the scrollview:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
// Figure out how much of the self.userFeedbackView is displayed.
CGRect frame = CGRectIntersection(self.scrollView.bounds, self.userFeedbackView.frame);
CGFloat proportion = (frame.size.height*frame.size.width)/(self.userFeedbackView.frameWidth*self.userFeedbackView.frameHeight);
NSLog(#"%f; %#", proportion, NSStringFromCGRect(frame));
}

Ole Begemann's answer in swift 5,
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.intersects(subViewFrame){
// do something
}
}
Note: Pls make sure that the subViewFrame is calculated with respect to the scrollView's frame.

Related

Rotate UIImageView inside UIScrollView in Swift

I'm working in a basic photo editor which is supposed to zoom, rotate and flip a photo. I'm using an image view (aspect fill) inside a scroll view which allows me to zoom easily. But when I try to rotate or flip the result is not what I would expect. The image view keeps the original frame and seems like rotating the image. The scroll view zoom scale changes. Any suggestions on how to do this?
It also would be great to have suggestions about setting the image view anchor point to match the scroll view anchor point before transforming cause I don't want to display a different portion of the image after transforming, just the same portion of the image, but rotated.
View stack before transform:
View stack after applying rotation:
My code so far:
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
setZoomScale()
scrollView.zoomScale = scrollView.minimumZoomScale
}
#IBAction func rotateAnticlockwise(_ sender: UIButton) {
rotationAngle -= 0.5
transformImage()
}
func transformImage(){
var transform = CGAffineTransform.identity
transform = transform.rotated(by: .pi * rotationAngle)
imageView.transform = transform
}
func setZoomScale(){
let imageSize = imageView.image!.size
let smallestDimension = min(imageSize.width, imageSize.height)
scrollView.minimumZoomScale = scrollView.bounds.width / smallestDimension
scrollView.maximumZoomScale = smallestDimension / scrollView.bounds.width
}
I think you are looking for, e.g. :
imageView.transform = CGAffineTransform(rotationAngle: 0.5)

iOS7 Status Bar like the native weather app

Does anyone know how can I reproduce a similar effect from the native iOS7 weather app?
Basically, the status bar inherits the view's background underneath, but the content doesn't show up.
Also, a 1 pixel line is drawn after the 20 pixels height of the status bar, only if some content is underlayed.
The best thing is to make it through the clipSubview of the view. You put your content into the view and make constraints to left/right/bottom and height. Height on scroll view you check is the cell has minus position and at that time you start to change the height of content (clip) view to get desired effect.
This is a real app you can download and take a look from www.fancyinteractive.com. This functionality will be available soon as next update.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *visibleCells = [convertorsTableView visibleCells];
if (visibleCells.count) {
for (CVConverterTableViewCell *cell in visibleCells) {
CGFloat positionYInView = [convertorsTableView convertRect:cell.frame toView:self.view].origin.y;
[self clipLayoutConstraint:cell.clipHeightLayoutConstraint withPosition:positionYInView defaultHeight:cell.frameHeight];
[cell.converterLabel layoutIfNeeded];
[cell.iconImageView layoutIfNeeded];
}
}
[self checkStatusBarSeperator:scrollView.contentOffset.y];
}
- (void)clipLayoutConstraint:(NSLayoutConstraint *)constraint withPosition:(CGFloat)position defaultHeight:(CGFloat)defaultHeight {
if (position < 0) {
constraint.constant = (defaultHeight - -position - 20 > 10) ? defaultHeight - -position - 20 : 10;
} else
constraint.constant = defaultHeight;
}
You can accomplish this by setting a mask to the table view's layer. You will not be able however to render the animations inside the cells, but you can do those yourself behind the table view, and track their movement with the table view's scrollview delegate methods.
Here is some informations on CALayer masks:
http://evandavis.me/blog/2013/2/13/getting-creative-with-calayer-masks
Swift 5:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visibleCells = tableView.visibleCells as? [TableViewCell] else { return }
let defaultClipHeight: CGFloat = 24
let statusBarHeight: CGFloat = UIApplication.statusBarHeight
if !visibleCells.isEmpty {
for cell in visibleCells {
let topSpace = cell.frame.size.height - defaultClipHeight - cell.clipBottomConstraint.constant
let cellOffsetY = tableView.contentOffset.y - cell.frame.origin.y + statusBarHeight
if cellOffsetY > topSpace {
let clipOffsetY = cellOffsetY - topSpace
let clipHeight = defaultClipHeight - clipOffsetY
cell.clipHeightConstraint.constant = max(clipHeight, 0)
} else {
cell.clipHeightConstraint.constant = defaultClipHeight
}
}
}
}
Starting Page:
Scrolling First Item:
Scrolling Second Item:

How to animate UITableView header view

I need to animate the insertion of a tableview header view. I want that the table rows to slide down while the header view expands its frame.
So far the best result I got is by using the following code:
[self.tableView beginUpdates];
[self.tableView setTableHeaderView:self.someHeaderView];
[self.tableView endUpdates];
The problem with this solution is that the header itself doesn't get animated, its frame doesn't expand.
So the effect I'm getting is that the table rows slide down (as I want) but the header view is immediately shown, and I want it to expand it's frame with animation.
Any ideas?
Thanks.
have the header frame at CGRectZero
and set its frame using animation
[self.tableView beginUpdates];
[self.tableView setTableHeaderView:self.someHeaderView];
[UIView animateWithDuration:.5f animations:^{
CGRect theFrame = someBigger.frame;
someHeaderView.frame = theFrame;
}];
[self.tableView endUpdates];
Here's what I got to work in Swift, with an initial frame height of zero and updating it to its full height in the animations closure:
// Calculate new frame size for the table header
var newFrame = tableView.tableHeaderView!.frame
newFrame.size.height = 42
// Get the reference to the header view
let tableHeaderView = tableView.tableHeaderView
// Animate the height change
UIView.animate(withDuration: 0.6) {
tableHeaderView.frame = newFrame
self.tableView.tableHeaderView = tableHeaderView
})
The selected answer didn't work for me. Had to do the following:
[UIView animateWithDuration: 0.5f
animations: ^{
CGRect frame = self.tableView.tableHeaderView.frame;
frame.size.height = self.headerViewController.preferredHeight;
self.tableView.tableHeaderView.frame = frame;
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}];
The really peculiar part is that this works in one view but not in a subclass of the same view controller / view hierarchy.
I found the definitive and proper way to achieve a real animation on tableHeaderViews!
• First, if you're in Autolayout, keep the Margin system inside your header view. With Xcode 8 its now possible (at last!), just do not set any constraint to any of the header subviews. You'll have to set the correct margins thought.
• Then use beginUpdates and endUpdates, but put endUpdate into the animation block.
// [self.tableView setTableHeaderView:headerView]; // not needed if already there
[self.tableView beginUpdates];
CGRect headerFrame = self.tableView.tableHeaderView.bounds;
headerFrame.size.height = PLAccountHeaderErrorHeight;
[UIView animateWithDuration:0.2 animations:^{
self.tableView.tableHeaderView.bounds = headerFrame;
[self.tableView endUpdates];
}];
Note: I only tried in iOS10, i'll post an update when my iOS9 simulator will be downloaded.
EDIT
Unfortunately, it doesn't work in iOS9 as well as iOS10: the header itself does not change its height, but header subviews seem to move as if the header bounds changed.
SWIFT 4 SOLUTION w/ Different Header
If you want this kind of animation, that is the solution that I have used: (even though in my case I had also to play a bit with the opacity of the objects, since I was loading another completely different header)
fileprivate func transitionExample() {
let oldHeight = tableView.tableHeaderView?.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height ?? 0
let newHeader = YourCustomHeaderView()
newHeader.hideElements() // If you want to play with opacity.
let smallHeaderFrame = CGRect(x: 0, y: 0, width: newHeader.frame.width, height: oldHeight)
let fullHeaderFrame = newHeader.frame
newHeader.frame = smallHeaderFrame
UIView.animate(withDuration: 0.4) { [weak self] in
self?.tableView.beginUpdates()
newHeader.showElements() // Only if have hided the elements before.
self?.tableView.tableHeaderView = newHeader
self?.tableView.tableHeaderView?.frame = fullHeaderFrame
self?.tableView.endUpdates()
}
}
So, basically, it's all about changing the frame inside the .animate closure. The beging/end updates is needed in order to move and update the other elements of the tableView.
Here is a Swift 5 UITableView extension for changing the headerView or changing its height, animated using AutoLayout
extension UITableView {
func animateTableHeader(view: UIView? = nil, frame: CGRect? = nil) {
self.tableHeaderView?.setNeedsUpdateConstraints()
UIView.animate(withDuration: 0.2, animations: {() -> Void in
let hView = view ?? self.tableHeaderView
if frame != nil {
hView?.frame = frame!
}
self.tableHeaderView = hView
self.tableHeaderView?.layoutIfNeeded()
}, completion: nil)
}
}
For me it is animated when using Auto Layout without any explicit animation block or manipulating frames.
// `tableHeaderViewHeight` is a reference to the height constraint of `tableHeaderView`
tableHeaderViewHeight?.constant = .leastNormalMagnitude
tableView.beginUpdates()
tableView.tableHeaderView = tableHeaderView
tableHeaderView.layoutIfNeeded()
tableView.endUpdates()
Use below code this works
extension UITableView {
func hideTableHeaderView() -> Void {
self.beginUpdates()
UIView.animate(withDuration: 0.2, animations: {
self.tableHeaderView = nil
})
self.endUpdates()
}
func showTableHeaderView(header: UIView) -> Void {
let headerView = header
self.beginUpdates()
let headerFrame = headerView.frame
headerView.frame = CGRect()
self.tableHeaderView = headerView
UIView.animate(withDuration: 0.2, animations: {
self.tableHeaderView?.frame = headerFrame
self.tableHeaderView?.alpha = 0
self.endUpdates()
}, completion: { (ok) in
self.tableHeaderView?.alpha = 1
})
}
}

UICollectionView: paging like Safari tabs or App Store search

I want to implement "cards" in my app like Safari tabs or App Store search.
I will show user one card in a center of screen and part of previous and next cards at left and right sides. (See App Store search or Safari tabs for example)
I decided to use UICollectionView, and I need to change page size (didn't find how) or implement own layout subclass (don't know how)?
Any help, please?
Below is the simplest way that I've found to get this effect. It involves your collection view and an extra secret scroll view.
Set up your collection views
Set up your collection view and all its data source methods.
Frame the collection view; it should span the full width that you want to be visible.
Set the collection view's contentInset:
_collectionView.contentInset = UIEdgeInsetsMake(0, (self.view.frame.size.width-pageSize)/2, 0, (self.view.frame.size.width-pageSize)/2);
This helps offset the cells properly.
Set up your secret scrollview
Create a scrollview, place it wherever you like. You can set it to hidden if you like.
Set the size of the scrollview's bounds to the desired size of your page.
Set yourself as the delegate of the scrollview.
Set its contentSize to the expected content size of your collection view.
Move your gesture recognizer
Add the secret scrollview's gesture recognizer to the collection view, and disable the collection view's gesture recognizer:
[_collectionView addGestureRecognizer:_secretScrollView.panGestureRecognizer];
_collectionView.panGestureRecognizer.enabled = NO;
Delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.x = contentOffset.x - _collectionView.contentInset.left;
_collectionView.contentOffset = contentOffset;
}
As the scrollview moves, get its offset and set it to the offset of the collection view.
I blogged about this here, so check this link for updates: http://khanlou.com/2013/04/paging-a-overflowing-collection-view/
You can subclass UICollectionViewFlowLayout and override like so:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)contentOffset
withScrollingVelocity:(CGPoint)velocity
{
NSArray* layoutAttributesArray =
[self layoutAttributesForElementsInRect:self.collectionView.bounds];
if(layoutAttributesArray.count == 0)
return contentOffset;
UICollectionViewLayoutAttributes* candidate =
layoutAttributesArray.firstObject;
for (UICollectionViewLayoutAttributes* layoutAttributes in layoutAttributesArray)
{
if (layoutAttributes.representedElementCategory != UICollectionElementCategoryCell)
continue;
if((velocity.x > 0.0 && layoutAttributes.center.x > candidate.center.x) ||
(velocity.x <= 0.0 && layoutAttributes.center.x < candidate.center.x))
candidate = layoutAttributes;
}
return CGPointMake(candidate.center.x - self.collectionView.bounds.size.width * 0.5f, contentOffset.y);
}
This will get the next or previous cell depending on the velocity... it will not snap on the current cell however.
#Mike M's answer in Swift…
class CenteringFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView,
let layoutAttributesArray = layoutAttributesForElementsInRect(collectionView.bounds),
var candidate = layoutAttributesArray.first else { return proposedContentOffset }
layoutAttributesArray.filter({$0.representedElementCategory == .Cell }).forEach { layoutAttributes in
if (velocity.x > 0 && layoutAttributes.center.x > candidate.center.x) ||
(velocity.x <= 0 && layoutAttributes.center.x < candidate.center.x) {
candidate = layoutAttributes
}
}
return CGPoint(x: candidate.center.x - collectionView.bounds.width / 2, y: proposedContentOffset.y)
}
}
A little edit on Soroush answer, which did the trick for me.
The only edit I made instead of disabling the gesture:
[_collectionView addGestureRecognizer:_secretScrollView.panGestureRecognizer];
_collectionView.panGestureRecognizer.enabled = NO;
I disabled scrolling on the collectionview:
_collectionView.scrollEnabled = NO;
As disabling the gesture disabled the secret scrollview gesture as well.
I'll add another solution. The snapping in place is not perfect (not as good as when paging enabled is set, but works well enough).
I have tried implementing Soroush's solution, but it doesn't work for me.
Because the UICollectionView is a subclass of UIScrollView it will respond to an important UIScrollViewDelegate method which is:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
The targetContentOffset (an inout pointer) lets you redefine the stopping point for a collection view (the x in this case if you swipe horizontally).
A quick note about a couple of the variables found below:
self.cellWidth – this is your collection view cell's width (you can even hardcode it there if you want)
self.minimumLineSpacing – this is the minimum line spacing you set between the cells
self.scrollingObjects is the array of objects contained in the collection view (I need this mostly for the count, to know when to stop scrolling)
So, the idea is to implement this method in the collection view's delegate, like so:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (self.currentIndex == 0 && velocity.x < 0) {
// we have reached the first user and we're trying to go back
return;
}
if (self.currentIndex == (self.scrollingObjects.count - 1) && velocity.x > 0) {
// we have reached the last user and we're trying to go forward
return;
}
if (velocity.x < 0) {
// swiping from left to right (going left; going back)
self.currentIndex--;
} else {
// swiping from right to left (going right; going forward)
self.currentIndex++;
}
float xPositionToStop = 0;
if (self.currentIndex == 0) {
// first row
xPositionToStop = 0;
} else {
// one of the next ones
xPositionToStop = self.currentIndex * self.cellWidth + (self.currentIndex + 1) * self.minimumLineSpacing - ((scrollView.bounds.size.width - 2*self.minimumLineSpacing - self.cellWidth)/2);
}
targetContentOffset->x = xPositionToStop;
NSLog(#"Point of stopping: %#", NSStringFromCGPoint(*targetContentOffset));
}
Looking forward to any feedback you may have which makes the snapping in place better. I'll also keep on looking for a better solution...
The previous is quite complicated, UICollectionView is a subclass of UIScrollView, so just do this:
[self.collectionView setPagingEnabled:YES];
You are all set to go.
See this detailed tutorial.

How to enable zoom in UIScrollView

How do I enable zooming in a UIScrollView?
Answer is here:
A scroll view also handles zooming and panning of content. As the user makes a pinch-in or pinch-out gesture, the scroll view adjusts the offset and the scale of the content. When the gesture ends, the object managing the content view should update subviews of the content as necessary. (Note that the gesture can end and a finger could still be down.) While the gesture is in progress, the scroll view does not send any tracking calls to the subview.
The UIScrollView class can have a delegate that must adopt the UIScrollViewDelegate protocol. For zooming and panning to work, the delegate must implement both viewForZoomingInScrollView: and scrollViewDidEndZooming:withView:atScale:; in addition, the maximum (maximumZoomScale) and minimum (minimumZoomScale) zoom scale must be different.
So:
You need a delegate that implements UIScrollViewDelegate and is set to delegate on your UIScrollView instance
On your delegate you have to implement one method: viewForZoomingInScrollView: (which must return the content view you're interested in zooming). You can also implement scrollViewDidEndZooming:withView:atScale: optionally.
On your UIScrollView instance, you have to set the minimumZoomScale and the maximumZoomScale to be different (they are 1.0 by default).
Note: The interesting thing about this is what if you want to break zooming. Is it enough to return nil in the viewForZooming... method? It does break zooming, but some of the gestures will be messed up (for two fingers). Therefore, to break zooming you should set the min and max zoom scale to 1.0.
Have a read through this Ray Wenderlich tutorial:
http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift
If you follow through the section 'Scrolling and Zooming a Larger Image' it will get a image up and enable you to pinch and zoom.
In case the link gets altered, here's the main info:
Put this code in your view controller (this sets the main functionality):
override func viewDidLoad() {
super.viewDidLoad()
// 1
let image = UIImage(named: "photo1.png")!
imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size:image.size)
scrollView.addSubview(imageView)
// 2
scrollView.contentSize = image.size
// 3
var doubleTapRecognizer = UITapGestureRecognizer(target: self, action: "scrollViewDoubleTapped:")
doubleTapRecognizer.numberOfTapsRequired = 2
doubleTapRecognizer.numberOfTouchesRequired = 1
scrollView.addGestureRecognizer(doubleTapRecognizer)
// 4
let scrollViewFrame = scrollView.frame
let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
let minScale = min(scaleWidth, scaleHeight);
scrollView.minimumZoomScale = minScale;
// 5
scrollView.maximumZoomScale = 1.0
scrollView.zoomScale = minScale;
// 6
centerScrollViewContents()
}
Add this to the class:
func centerScrollViewContents() {
let boundsSize = scrollView.bounds.size
var contentsFrame = imageView.frame
if contentsFrame.size.width < boundsSize.width {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0
} else {
contentsFrame.origin.x = 0.0
}
if contentsFrame.size.height < boundsSize.height {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0
} else {
contentsFrame.origin.y = 0.0
}
imageView.frame = contentsFrame
}
And then this if you want the double tap gesture to be recognised:
func scrollViewDoubleTapped(recognizer: UITapGestureRecognizer) {
// 1
let pointInView = recognizer.locationInView(imageView)
// 2
var newZoomScale = scrollView.zoomScale * 1.5
newZoomScale = min(newZoomScale, scrollView.maximumZoomScale)
// 3
let scrollViewSize = scrollView.bounds.size
let w = scrollViewSize.width / newZoomScale
let h = scrollViewSize.height / newZoomScale
let x = pointInView.x - (w / 2.0)
let y = pointInView.y - (h / 2.0)
let rectToZoomTo = CGRectMake(x, y, w, h);
// 4
scrollView.zoomToRect(rectToZoomTo, animated: true)
}
If you want more detail read the tutorial, but that pretty much covers it.
Make sure you set your viewController as the scrollViews delegate and implement:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
I don't think this is working for iOS 5.0 and Xcode 4.3+
Im looking for the same here, I found this its for images but it may help you.
http://www.youtube.com/watch?v=Ptm4St6ySEI