i'm trying to create a scrollView with a UIPageControl. This works fine, but the problem is the images wont center in each page of the scrollView. it is aligned to the right. How can i center the images always?
image illustration:
http://i.stack.imgur.com/In6t7.png
code:
viewDidLoad
self.pageControl = UIPageControl()
self.pageControl?.frame = CGRectZero
self.pageControl?.currentPage = 0
self.pageControl?.numberOfPages = self.imageArray!.count
self.pageControl?.tintColor = UIColor.whiteColor()
self.pageControl?.userInteractionEnabled = false
self.navigationItem.titleView = self.pageControl?
for var i = 0; i<self.imageArray?.count; i++ {
self.pageViews.append(nil)
}
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSizeMake(pagesScrollViewSize.width * CGFloat(self.imageArray!.count), pagesScrollViewSize.height)
self.loadVisiblePages()
rest of the methods
func loadPage(page: Int) {
if page < 0 || page >= self.imageArray!.count {
// If it's outside the range of what you have to display, then do nothing
return
}
// 1
if let pageView = pageViews[page] {
// Do nothing. The view is already loaded.
} else {
// 2
let newPageView = UIImageView(image: self.imageArray![page] as UIImage)
var frame = scrollView.bounds
frame.origin.x = (frame.size.width * CGFloat(page))
frame.origin.y = 0.0
// 3
newPageView.contentMode = .ScaleAspectFit
newPageView.frame = frame
scrollView.addSubview(newPageView)
newPageView.backgroundColor = UIColor.blackColor()
// 4
pageViews[page] = newPageView
}
}
func purgePage(page: Int) {
if page < 0 || page >= self.imageArray!.count {
// If it's outside the range of what you have to display, then do nothing
return
}
// Remove a page from the scroll view and reset the container array
if let pageView = pageViews[page] {
pageView.removeFromSuperview()
pageViews[page] = nil
}
}
func loadVisiblePages() {
// First, determine which page is currently visible
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
// Update the page control
pageControl?.currentPage = page
// Work out which pages you want to load
let firstPage = page - 1
let lastPage = page + 1
// Purge anything before the first page
for var index = 0; index < firstPage; ++index {
purgePage(index)
}
// Load pages in our range
for var index = firstPage; index <= lastPage; ++index {
loadPage(index)
}
// Purge anything after the last page
for var index = lastPage+1; index < self.imageArray!.count; ++index {
purgePage(index)
}
}
func scrollViewDidScroll(scrollView: UIScrollView!) {
// Load the pages that are now on screen
loadVisiblePages()
}
I found here you use let to declare a constant UIImageView, I think you should use var.
let newPageView = UIImageView(image: self.imageArray![page] as UIImage)
Actually, I can find any problem besides this, I think the reason you can't set your image to the center because that all the operation after this declaration didn't work, so you can't set the right frame, the right contentMode to the UIImageView.
Perhaps it would be easier if you use the center.x instead of origin.x when positioning your scrollView's image views.
The formula I would use in this case:
centerX = pageNumber * 320 + 160
So something like:
myImageView.center = CGPointMake(pageNumber * 320 + 160, 0);
Running through my formula, I get these result:
page 0: 0 * 320 + 160 = 160
page 1: 1 * 320 + 160 = 480
page 2: 2 * 320 + 160 = 800
page 3: 3 * 320 + 160 = 1120
You can see each page has an offset of 320 (assuming iPhone screen dimension here). You would obviously use this instead:
self.view.bounds.size.width
The 320 here is for illustration purpose.
Your Method
OK, so lets run through your code:
var frame = scrollView.bounds
frame.origin.x = (frame.size.width * CGFloat(page))
frame.origin.y = 0.0
First time, origin.x would be 0 if page starts from 0, scrollView's width remains 320.
Second time, origin.x would be 320 * 1 (page == 1 here), scrollView's width will become 640 since you have 2 imageViews that are 320 wide I assume.
Third time, origin.x would be 640 * 2 = 1280, scrollView's width is now 1600 (origin.x = 1280 + 320 imageView width)
You see how the numbers keeps getting a lot bigger instead of a constant 320?
Related
I've subclassed a UICollectionViewFlowLayout to achieve a small scaling effect during horizontal scroll. Therefore I had to subclass a UICollectionViewCell as well as to change the layer.anchorPoint of the cell (my scaling is from the bottom left of the cell rather than from the default center). Now all fine and well except the fact that when I am scrolling horizontally , my cell is reused too soon (I still can see the half cell when it suddenly disappear ).
I have the feeling that collection view still bases its calculations for reusing the cell on the anchor point positioned in the center of the cell...
However , this is my collection view . You can see how the item getting bigger as it reaches the left side of the collection view. This is the scaling I wanted.
Now here I am scrolling to the left. You can see how the right item became bigger and the left is getting out of the screen.
And here you see that the left item didn't get off the screen but already dissapeared. Only the right item remeained visible :/
So what I want is , to make the left item disappear only when it's right boundaries reaching the very left of the screen.Simply saying , to dissapear only when I don't see it anymore.
And here is my code:
class SongsCollectionViewCell : UICollectionViewCell {
#IBOutlet weak var imgAlbumCover: UIImageView!
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) {
super.applyLayoutAttributes(layoutAttributes)
//we must change the anchor point for propper cells positioning and scaling
self.layer.anchorPoint.x = 0
self.layer.anchorPoint.y = 1
}
}
Here is the layout itself :
class SongsCollectionViewLayout : UICollectionViewFlowLayout {
override func prepareLayout() {
collectionView?.decelerationRate = UIScrollViewDecelerationRateFast
self.scrollDirection = UICollectionViewScrollDirection.Horizontal;
//size of the viewport
let size:CGSize = self.collectionView!.frame.size;
let itemWidth:CGFloat = size.width * 0.7//0.7//3.0;
self.itemSize = CGSizeMake(itemWidth, itemWidth);
self.sectionInset = UIEdgeInsetsMake(0,0,0,0);
self.minimumLineSpacing = 0
self.minimumInteritemSpacing = 0
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes:[UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)!
var visibleRect:CGRect = CGRect()
visibleRect.origin = self.collectionView!.contentOffset;
visibleRect.size = self.collectionView!.bounds.size;
for layoutAttributes in attributes {
if (CGRectIntersectsRect(layoutAttributes.frame, rect)) {
//we must align items to the bottom of the collection view on y axis
let frameHeight = self.collectionView!.bounds.size.height
layoutAttributes.center.y = frameHeight
layoutAttributes.center.x = layoutAttributes.center.x - self.itemSize.width/2
//find where ite, left x is
let itemLeftX:CGFloat = layoutAttributes.center.x
//distance of the item from the left of the viewport
let distanceFromTheLeft:CGFloat = itemLeftX - CGRectGetMinX(visibleRect)
let normalizedDistanceFromTheLeft = abs(distanceFromTheLeft) / self.collectionView!.frame.size.width
//item that is closer to the left is most visible one
layoutAttributes.alpha = 1 - normalizedDistanceFromTheLeft
layoutAttributes.zIndex = abs(Int(layoutAttributes.alpha)) * 10;
//scale items
let scale = min(layoutAttributes.alpha + 0.5, 1)
layoutAttributes.transform = CGAffineTransformMakeScale(scale, scale)
}
}
return attributes;
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// Snap cells to centre
var newOffset = CGPoint()
let layout = collectionView!.collectionViewLayout as! UICollectionViewFlowLayout
let width = layout.itemSize.width + layout.minimumLineSpacing
var offset = proposedContentOffset.x + collectionView!.contentInset.left
if velocity.x > 0 {
//ceil returns next biggest number
offset = width * ceil(offset / width)
} else if velocity.x == 0 { //6
//rounds the argument
offset = width * round(offset / width)
} else if velocity.x < 0 { //7
//removes decimal part of argument
offset = width * floor(offset / width)
}
newOffset.x = offset - collectionView!.contentInset.left
newOffset.y = proposedContentOffset.y //y will always be the same...
return newOffset
}
}
Answering my own question.
So as I suspected , the layout was taking an old center into account that is why I had to correct the center of the cell right after changing the anchor point . So my custom cell now looks like this :
class SongsCollectionViewCell : UICollectionViewCell {
#IBOutlet weak var imgAlbumCover: UIImageView!
override func prepareForReuse() {
imgAlbumCover.hnk_cancelSetImage()
imgAlbumCover.image = nil
}
override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) {
super.applyLayoutAttributes(layoutAttributes)
//we must change the anchor point for propper cells positioning and scaling
self.layer.anchorPoint.x = 0
self.layer.anchorPoint.y = 1
//we need to adjust a center now
self.center.x = self.center.x - layoutAttributes.size.width/2
}
}
Hope it helps someone
i have built an app that allows the user to take their picture with a selection of glasses overlaid. it is working exactly how i want it on both an ipad and ipad mini but on the iphone the position in relation the the scroll view is wrong (see Images).
func setupPages(){
pageImages = [
UIImage(named: "glasses_one")!,
UIImage(named: "glasses_two")!,
UIImage(named: "glasses_three")!,
UIImage(named: "glasses_four")!,
UIImage(named: "glasses_five")!,
UIImage(named: "glasses_six")!
]
let pageCount = pageImages.count
for _ in 0..<pageCount {
pageViews.append(nil)
}
print(pageViews.count)
let pagesScrollViewSize = scrollView.frame.size
scrollView.contentSize = CGSize(width: scrollWidth * CGFloat(pageImages.count),
height: pagesScrollViewSize.height)
scrollView.backgroundColor = UIColor.blueColor()
loadVisiblePages()
}
func loadVisiblePages() {
let pageWidth = scrollView.frame.size.width
page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
print(page)
pageControl.currentPage = page
for index in 0...5 {
//print(" -- Index = \(index)")
loadPage(index)
}
}
func loadPage(page: Int) {
if page < 0 || page >= pageImages.count {
// If it's outside the range of what you have to display, then do nothing
return
}
// 1
if let pageView = pageViews[page] {
print(pageView.description)
// Do nothing. The view is already loaded.
} else {
// 2
var frame = scrollView.bounds
frame.origin.x = scrollWidth * CGFloat(page)
frame.origin.y = 0.0
frame.size.width = scrollWidth
// 3
let newPageView = UIImageView(image: pageImages[page])
newPageView.contentMode = .ScaleAspectFit
newPageView.frame = frame
newPageView.clipsToBounds = false
scrollView.addSubview(newPageView)
// 4
pageViews[page] = newPageView
}
}
Ipad
Iphone
I think your issue is you need to add a constraint to center you image vertically. Or you could also solve this by making the frame for your "glasses frame" A set width and height. Smaller screens will have less space around it but will still be in the middle.
I have swift 2.0 ios app with storyboard. I have a tableViewController and in each row I have scrollView (with user scrolling disabled). Inside this scrollView I have an image, which fits scroll's size and it's a bit taller. Thing is I want to achieve "floating" effect on this image, when tableView is scrolled.
Here's a code:
override func scrollViewDidScroll(scrollView: UIScrollView) {
let cells = tableView.visibleCells
let offsetY = scrollView.contentOffset.y
var diff:CGFloat = (offsetY - lastY) / 10
for cell in cells {
let indexPath = tableView.indexPathForCell(cell)!
let scroll = cell.viewWithTag(102) as! UIScrollView
let oldY = scroll.contentOffset.y
let newY = oldY + diff
let height = scroll.contentSize.height
if(newY >= 0 && newY + cellHeight < height){
scroll.contentOffset = CGPointMake(scroll.contentOffset.x, newY)
}
if(indexPath.row == 0){
print("\(oldY) + \(diff) = \(newY)")
}
}
lastY = offsetY
}
What I'm doing here is calculating difference of tableView scroll position (variable "diff"), get all visible cells and scroll scrollView's by that difference (divided by 10 and in opposite direction). When I scroll fast it's somehow working, but when I scroll very slowly nothing is moving.
When I'm slowly scrolling I'm getting output like this (last print):
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
When diff is too small scrollView is somehow truncating (or rounding) a result.
When I'm scrolling faster:
23.0 + 0.25 = 23.25
23.5 + 0.25 = 23.75
24.0 + 0.15 = 24.15
24.0 + 0.25 = 24.25
24.5 + 0.25 = 24.75
25.0 + 0.25 = 25.25
25.5 + 0.25 = 25.75
Here you can see it's suming up, but some results does not match (sum of line x should be first number in the next line).
It's not an issue with CGFloat as simple example shows:
var sum:CGFloat = 0
for(var i = 0; i < 10; i++){
let number:CGFloat = 0.05
sum += number
print(sum)
}
Results in:
0.05
0.1
0.15
0.2
0.25
0.3
0.35
0.4
0.45
0.5
How can I fix it?
Maybe I should use a different approach to get similar effect?
UIScrollView.contentOffset is rounded to 0.5 on retina displays (0.33 on iPhone 6+) when set.
I suggest creating custom UITableViewCell subclass (if you don't have it yet) and create custom variable inside it:
class MyCell: UITableViewCell {
var unroundedContentOffsetY: CGFloat {
didSet {
scroll.contentOffset = CGPoint(x: scroll.contentOffset.x, unroundedContentOffsetY)
}
}
}
Then make calculations on cell.unroundedContentOffsetY variable instead of cell.scroll.contentOffset
Apparently one can also override the contentOffset property in the custom subclass like this:
var unrounded: CGPoint?
override var contentOffset: CGPoint {
get {
return unrounded ?? .zero
}
set {
unrounded = newValue
super.contentOffset = newValue
}
}
This has the advantage that it also works when calling setContentOffset(_:animated:), not only when assigning to contentOffset.
I am trying to horizontally center 30 images in a scrollview. What is the best approach to do so? Thanks.
EDIT: Programmatically I was able to create the images, thanks to David Cao, but I am still having the issue of it not centering. Thanks.
If they're all the same image, why not do this programmatically? Just make a for loop and iterate through the bounds.
NSInteger numWidth = 3;
NSInteger numHeight = 10;
CGFloat border = 8;
CGFloat width = (self.scrollView.frame.size.width - (numWidth + 1) * border)/3;
for (NSInteger i = 0; i < numWidth; ++i) {
for (NSInteger j = 0; j < numHeight; ++j) {
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"imageNameHere"]];
imageView.frame = CGRectMake(border + width*i, border + width*j, width, width);
[self.scrollView addSubview:imageView];
}
}
[self.scrollView setContentSize:CGSizeMake(self.scrollView.frame.size.width, (border + width)*numHeight + border];
I was able to solve this problem. I modified David Cao's code to account for various screen widths using
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width`
I adjusted the scrollView frame size width to equal the screenWidth. Removing the border variable fit my need for fitting the screen perfectly.
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
//let screenHeight = screenSize.height
let numWidth: CGFloat = 3
let numHeight: CGFloat = 10
self.scrollView.frame.size.width = screenWidth
let width: CGFloat = (self.scrollView.frame.size.width - (numWidth + 1))/3
for var i:CGFloat = 0; i < 3; ++i{
for var j:CGFloat = 0; j < 10; ++j {
let image: UIImage = UIImage(named: "image1.png")!
imageView = UIImageView(image: image)
imageView!.frame = CGRectMake(width*i, width*j, width, width)
self.scrollView.addSubview(imageView!)
}
}
scrollView.contentSize.height = (width)*numHeight
Thank you all for the help!
I would like my scrollview's frame to be the same size as the superview's frame. And set the contentsize so that it contains all the imageViews.
The problem is with AutoLayout, the width and height is 600 x 480, I use an iphone5 and the size class is w:Compact / h:Any.(I expected something like : 320 x 523 ).
My constraints are : Equal Width with superview, Equal Height with superview (multiplier 0.8), horizontal center X, and vertical center Y.
let pagesScrollViewSize = scrollView.frame.size
println("pagesScrollViewSize : \(pagesScrollViewSize)") //600x480
scrollView.contentSize = CGSizeMake(pagesScrollViewSize.width * CGFloat(pageCount), pagesScrollViewSize.height)
Would someone know how to solve this? The imageview appears bigger than the frame of the screen, so the .SizeAspectFit does not follow the right dimensions. Instead of scrolling to another imageView, it scrolls to see the rest of the image, which I don't want.
EDIT:
This is the whole code in ViewController.swift :
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var scrollView: UIScrollView!
var pageViews: [UIImageView?] = []
var pageImages : [UIImage?] = []
override func viewDidLoad() {
super.viewDidLoad()
pageImages = [UIImage(named:"photo1.png"),
UIImage(named:"photo2.png"),
UIImage(named:"photo3.png"),
UIImage(named:"photo4.png")]
let pageCount = pageImages.count
pageControl.currentPage = 0
pageControl.numberOfPages = pageCount
for _ in 0..<pageCount {
pageViews.append(nil)
}
let pagesScrollViewSize = scrollView.frame.size
println("pagesScrollViewSize : \(pagesScrollViewSize)")
scrollView.contentSize = CGSizeMake(pagesScrollViewSize.width * CGFloat(pageCount), pagesScrollViewSize.height)
loadVisiblePages()
}
//when scrolling
func scrollViewDidScroll(scrollView: UIScrollView!){
loadVisiblePages()
}
func loadVisiblePages(){
let pageWidth = scrollView.frame.size.width
let page = Int(floor( (scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth*2.0) ))
println("page: \(page)")
pageControl.currentPage = page
let firstPage = page-1
let lastPage = page+1
//remove all the pages before firstPage
for var index = 0; index < firstPage; ++index{
purgePage(index)
}
//load pages in our range
for var index = firstPage; index <= lastPage; ++index {
loadPage(index)
}
//remove after lastPage
for var index = lastPage+1; index < pageImages.count; ++index {
purgePage(index)
}
}
func loadPage(index:Int){
if index < 0 || index >= pageImages.count {
return
}
if let pageView = pageViews[index] {
//already loaded
}
else {
var frame = scrollView.bounds
frame.origin.x = frame.size.width * CGFloat(index)
frame.origin.y = 0.0
println("\(frame)")
var newImageView = UIImageView(image:pageImages[index])
newImageView.contentMode = .ScaleAspectFit
newImageView.frame = frame
scrollView.addSubview(newImageView)
pageViews[index] = newImageView
}
}
func purgePage(index:Int){
if index < 0 || index >= pageImages.count {
return
}
if let pageView = pageViews[index] {
pageView.removeFromSuperview()
pageViews[index] = nil
}
}
}
Thanks
Size classes (or any auto layout changes, for that matter) are not applied until after viewDidAppear or viewDidLayoutSubviews. You will need to move this code into one of those. You are seeing the frame of the view as it is loaded out of the Interface Builder document (xib or storyboard). You should never be checking frames until after those lifecycle events.
Unable to set frame correctly before viewDidAppear