I have the following function that gets called in my viewController's viewDidLoad:
fileprivate func setupScrollView(){
//this will be the height of the image on this ViewController
var imageHeight = ScreenSize.height() - 195 // replace with a relative calculation
if ScreenSize.model() == .iphoneX{
imageHeight = ScreenSize.height() * 0.58
}
self.dressImageScrollViewHeight.constant = imageHeight
//now determine the image width based on height
let scale = imageHeight / (self.item?.thumbnailSize?["height"] as? CGFloat ?? 1)
let newWidth = (self.item?.thumbnailSize?["width"] as? CGFloat ?? 0) * scale
self.imageScrollView.contentSize.width = newWidth + 20
self.imageScrollView.contentInset.left = 10
self.imageScrollView.contentInset.right = 20
self.imageScrollView.contentOffset.x = -10
guard self.firstImage != nil else{return}
//this is the imageView for the first image
// utilizes the preview image used on the cell in the previous view controller collection
let firstImageView = PFImageView(frame: CGRect(x: 0, y: 0, width: newWidth, height: imageHeight))
firstImageView.image = self.firstImage
self.imageScrollView.imageViews.append(firstImageView)
self.imageScrollView.addSubview(firstImageView)
}
Basically, I have something similar to a master detail view going on. When in the master view, the a cell is selected, it goes to this detail view that has a collection for images. The image used in the master view controller's cell for that item is passed over and is appended as the first image in the detail's image collection.
Recently, I've seen an influx in the number of crashes related to this code, by like a lot. I am pretty sure it has to do with the last few lines of the function.
Below is the insights that my Crashlytics provides.
The thing is, the Crashlytics logs point to that guard statement as the issue, except I can't at all re-create this issue, but it is certainly happening to my users, as these crash reports are coming in all the time now.
Parse's PFImageView: https://github.com/parse-community/ParseUI-iOS/blob/master/ParseUI/Classes/Views/PFImageView.m
Related
I want to recreate the same concept that the iOS built-in gallery app does in a basic level, that means having an array of images and being able to look at them individually and swiping right or left to see the other ones in the array. For testing purpose, I've set a small array of UIImages, and then added an individual subview for every image that there is in the array to a scrollView with paging enabled. Everything works as expected, however every time I load the app, the user starts from the first item (item #0 in the array) and I don't find a way to select manually the first image that needs to be shown, for example what if I want the scrollView to first show the third image in the array, and then be able to swipe left or right to page to the other images in the array? I can't find a solution to it.
I've tried to add other subviews but this doesn't work, the app works as expected but always starts from the first item in the array, I want to manually select the item so if I want to display the third image in the array, the collectionView does it like so. I have six images for testing purposes (each called menu_icon_(0...5))
#IBOutlet weak var scrollView: UIScrollView! {
didSet {
scrollView.delegate = self
}
}
var images:[ImagePreviewView] = [ImagePreviewView]()
override func viewDidLoad() {
super.viewDidLoad()
images = generateImages()
setupSlideScrollView(imagesPreviews: images)
}
func generateImages() -> [ImagePreviewView]{
var array:[ImagePreviewView] = []
for i in 0...5 {
let imagePreview = Bundle.main.loadNibNamed("ImagePreview", owner: self, options: nil)?.first as! ImagePreviewView
imagePreview.userSelectedImageView.image = UIImage(named: "menu_icon_\(i)")
array.append(imagePreview)
}
return array
}
func setupSlideScrollView(imagesPreviews : [ImagePreviewView]) {
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(imagesPreviews.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0 ..< imagesPreviews.count {
imagesPreviews[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(imagesPreviews[i])
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
print("Page Index is \(pageIndex)")
}
I want to implement this code in such a way that the user adds images to a collection view, and when the user taps in an image, that indexPath.item is sent to this viewController and shows the image that the user selected, not the first image in the array, and then is able to swipe right or left. I'm trying to recreate how it would be only by the latter view controller logic.
So you will receive the index when this VC is created. Lets call it selectedIndex
var selectedIndex = 0 //I give 0 as initial value but it will be updated anyways.
Then in viewDidAppear try with following:
let width = view.frame.width
scrollView.setContentOffset(CGPoint(x: width * selectedIndex, y: 0), animated: false)
So when a user taps to an image you will receive the index number of that image in your VC, then setContentOffset of scrollView to width of the screen times selectedIndex, this should give you the result you want.
I'm facing a weird issue that I can't seem to figure out or find anything about online.
So I'm trying to replicate the Shazam discover UI with a UICollectionView and a custom UICollectionViewFlowlayout.
So far everything is working pretty well, but when adding the "card stack" effect I (or rather the person who was implementing it) noticed there seems to be a weird issue where on some occasions (or rather, when specific indexes are visible, in the example it's row 5, 9) there will be 4 visible cells instead of 3. My guess would be that this has something to do with cell reuse, but I'm not sure why it's doing this. I looked into the individual cell dimensions and they all seem to be the same so it's not that cells are just sized differently.
Does anyone have an idea as to why this could be happening? Any help or suggestions are really appreciated.
I'll add a code snippet of the custom flowlayout and screenshots below.
You can download the full project here, or alternatively, check out the PR on Github.
Here's a visual comparison:
Source code of the custom flowlayout:
import UIKit
/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {
/// This property sets the amount of scaling for the first item.
internal var firstItemTransform: CGFloat?
/// This property enables paging per card. The default value is true.
internal var isPagingEnabled: Bool = true
/// Stores the height of a CardCell.
internal var cellHeight: CGFloat!
internal override func prepare() {
super.prepare()
assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
}
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSMutableArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
items.enumerateObjects(using: { (object, index, stop) -> Void in
let attributes = object as! UICollectionViewLayoutAttributes
self.updateCellAttributes(attributes)
})
return items as? [UICollectionViewLayoutAttributes]
}
// We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// Cell paging
internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
guard isPagingEnabled else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page height used for estimating and calculating paging.
let pageHeight = cellHeight + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.4
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newVerticalOffset.
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// make sure the zIndex of the next card is higher than the one we're swiping away.
let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
nextAttr?.zIndex = nextIndexPath.row
// attributes for swiping card away
let attr = self.layoutAttributesForItem(at: itemIndexPath)
return attr
}
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
// create stacked effect (cards visible at bottom
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
t = t.translatedBy(x: 0, y: (translationScale + deltaY * translationScale))
attributes.transform = t
}
origin.x = (self.collectionView?.frame.width)! / 2 - attributes.frame.width / 2 - (self.collectionView?.contentInset.left)!
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
}
edit 1: Just as an extra clarification, the final end result should make it look something like this:
edit 2:
Seems to be happening every 4-5 cards you scroll from my testing.
You have a layout that inherits from flow layout. You overrode layoutAttributesForElements(in rect:) where you take all the elements from super.layoutAttributesForElements and then for each one modify the properties in the method updateCellAttributes.
This is generally a good way to make a subclass of a flow layout. The UICollectionViewFlowLayout is doing most of the hard work - figuring out where each element should be, which elements are in the rect, what their basically attributes are, how they should be padded, etc, and you can just modify a few properties after the "hard" work is done. This works fine when you are adding a rotation or opacity or some other feature that does not change the location of the item.
You get into trouble when you change the items frame with updateCellAttributes. Then you can have a situation when you have a cell that would not have appeared in the frame at all for the regular flow layout, but now SHOULD appear because of your modification. So the attribute is not being returned AT ALL by super.layoutAttributesForElements(in rect: CGRect) so they don't show up at all. You can also have the opposite problem that cells that should not be in the frame at all are in the view but transformed in a way that cannot be seen by the user.
You haven't explained enough of what effect you are trying to do and why you think inheriting from UIFlowLayout is correct for me to be able to specifically help you. But I hope that I have given you enough information that you can find the problem on your own.
The bug is on how you defined frame.origin.y for each attribute. More specifically the value you hold in minY and determines how many of the cells you're keeping on screen. (I will edit this answer and explain more later but for now, try replacing the following code)
var minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
if minY > attributes.frame.origin.y + attributes.bounds.height + minimumLineSpacing + collectionView!.contentInset.top {
minY = 0
}
I need to animate the scroll view to show the same views but with different values based on the selection on the segmented control.I need left to right & right to left slide animation to show different values on the same UI. I dont want to create duplicate views.
I had the exact same problem. Wanted to avoid creating multiple tableviews just for animation sake.
Here is how I solved it :
Basic idea : Take a screenshot of tableview and display it on the root view. Then, do a slide animation of tableview from Left to Right (or Right to left depending on the switching of segment control)
Code when swiped to right
func switchToLeft() {
let newIndex = currentIndex - 1
if newIndex > 0 {
let sS = showScreenShot()
self.tableView.animateFromLeft(0.5){
sS.removeFromSuperview()
}
//update data and RELOAD your tableview here
}
}
func showScreenShot() -> UIView{
let image = getScreenShot()
let imageView = UIImageView(image: image)
let blankView = UIView(frame: self.view.frame)
blankView.addSubview(imageView)
self.tableView.superview?.addSubview(blankView)
self.tableView.superview?.bringSubviewToFront(self.tableView)
return blankView
}
func getScreenShot() -> UIImage?{
let viewToRender = self.tableView
let contentOffset = self.tableView.contentOffset
UIGraphicsBeginImageContext(viewToRender.bounds.size)
let ctx = UIGraphicsGetCurrentContext()!
// need to translate the context down to the current visible portion of the tableview
CGContextTranslateCTM(ctx, 0, -contentOffset.y-tableHeaderHt)
viewToRender.layer.renderInContext(ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Leaving off code for animateFromLeft as an exercise!
Strctly following this tutorial, in section Paging with UIScrollView, I have just implemented a ScrollView to use as a slideshow with downloaded photos from a previous UICollectionViewController. When scroll view is loaded, it does not work well because I see these ones:
Instead, when I slide back the images they are displayed in the correct way, one for each page. Or better, this problem disappears when I get the 4-th image in the slideshow, and only at that point all the following ones are correct, and so are the previous too. It is a problem which affects the first 2 or 3 images.
Moreover the slideshow does not even start from the UICollectionViewCell the user has just tapped, but always from the first. You can read all the code here:
import Foundation
import UIKit
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
#IBOutlet var pageControl: UIPageControl!
/* This will hold all the images to display – 1 per page.
It must be set from the previous view controller in prepareforsegue() method:
it will be the array of downloaded images for that photo gallery */
var pageImages:[UIImage]!
/* position in array images of the first to be showed, i.e. the one the user has just tapped */
var firstToShow:Int!
var currentImageViewForZoom:UIImageView?
/* This will hold instances of UIImageView to display each image on its respective page.
It’s an array of optionals, because you’ll be loading the pages lazily (i.e. as and when you need them)
so you need to be able to handle nil values from the array. */
var pageViews:[UIImageView?] = []
override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.delegate = self
self.scrollView.maximumZoomScale = 1.0
self.scrollView.zoomScale = 10.0
self.pageControl.numberOfPages = self.pageImages.count
self.pageControl.currentPage = self.firstToShow
for _ in 0..<self.pageImages.count {
self.pageViews.append(nil)
}
/* The scroll view, as before, needs to know its content size.
Since you want a horizontal paging scroll view, you calculate the width to be the number of pages multiplied by the width of the scroll view.
The height of the content is the same as the height of the scroll view
*/
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(self.pageImages.count),
height: pagesScrollViewSize.height)
// You’re going to need some pages shown initially, so you call loadVisiblePages()
self.loadVisiblePages()
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return self.currentImageViewForZoom
}
func scrollViewDidScroll(scrollView: UIScrollView) {
self.loadVisiblePages()
}
/*
Remember each page is a UIImageView stored in an array of optionals.
When the view controller loads, the array is filled with nil.
This method will load the content of each page.
1 - If it's outside the range of what you have to display, then do nothing
2 - If pageView is nil, then you need to create a page. So first, work out the frame for this page.
It’s calculated as being the same size as the scroll view, positioned at zero y offset,
and then offset by the width of a page multiplied by the page number in the x (horizontal) direction.
3 - Finally, you replace the nil in the pageViews array with the view you’ve just created,
so that if this page was asked to load again, you would now not go into the if statement and instead do nothing,
since the view for the page has already been created
*/
func loadPage(page: Int) {
if page < 0 || page >= self.pageImages.count {
//1
return
}
//2
if let _ = self.pageViews[page] {/*Do nothing. The view is already loaded*/}
else {
// 2
var frame = self.scrollView.bounds
frame.origin.x = frame.size.width * CGFloat(page)
frame.origin.y = 0.0
let newPageView = UIImageView(image: self.pageImages[page])
newPageView.contentMode = .ScaleAspectFit
newPageView.frame = frame
self.scrollView.addSubview(newPageView)
// 3
self.pageViews[page] = newPageView
self.currentImageViewForZoom = newPageView
}
}
/*
This function purges a page that was previously created via loadPage().
It first checks that the object in the pageViews array for this page is not nil.
If it’s not, it removes the view from the scroll view and updates the pageViews array with nil again to indicate that this page is no longer there.
Why bother lazy loading and purging pages, you ask?
Well, in this example, it won’t matter too much if you load all the pages at the start, since there are only five and they won’t be large enough to eat up too much memory.
But imagine you had 100 pages and each image was 5MB in size. That would take up 500MB of memory if you loaded all the pages at once!
Your app would quickly exceed the amount of memory available and be killed by the operating system.
Lazy loading means that you’ll only have a certain number of pages in memory at any given time.
*/
func purgePage(page: Int) {
if page < 0 || page >= self.pageImages.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 = self.pageViews[page] {
pageView.removeFromSuperview()
self.pageViews[page] = nil
}
}
func loadVisiblePages() {
// First, determine which page is currently visible
let pageWidth = self.scrollView.frame.size.width
// floor() function will round a decimal number to the next lowest integer
let page = Int(floor((self.scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0))) /***/
// Update the page control
self.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 {
self.purgePage(index)
}
// Load pages in our range
for index in firstPage...lastPage {
self.loadPage(index)
}
// Purge anything after the last page
for var index = lastPage+1; index < self.pageImages.count; ++index {
self.purgePage(index)
}
}
}
I guess the problem could be the line with /***/ which is something I have not understood from the tutorial. Thank you for your attention
UPDATE
Looking here in SO for similar posts, someone advised to create subviews in viewDidLayoutSubviews(), so here's what I have just tried:
override func viewDidLayoutSubviews() {
self.loadVisiblePages()
}
and now the images are correctly displayed and there's no more that strange effect for the first 3 images. But why? I am a junior iOS developer and I still don't know all those methods to override and the order in which they work. Anyway, the other problem which persists is that images are showed always from the first, even if another image is tapped. For example, have a look at this:
always the first image (left up corner) is displayed even if another one is tapped. And finally, in my code I implemented the delegate method to have zoom but it does not work too.
UPDATE 2
Here's the code of prepareForSegue() from the previous UICollectionViewController when the user taps a cell:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "toSlideShow") {
let pagedScrollViewController:PagedScrollViewController = segue.destinationViewController as! PagedScrollViewController
pagedScrollViewController.pageImages = self.imagesDownloaded
pagedScrollViewController.firstToShow = self.collectionView?.indexPathsForSelectedItems()![0].row
}
}
You should update the scroll view's offset to the be equal to the offset of the image you want to be showing after you transition. self.scrollView.contentOffset = CGPoint(x: pagesScrollViewSize.width * CGFloat(self.firstToShow), y: 0.0) You can do this in viewDidLayoutSubviews, which would make it look like:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.delegate = self
self.scrollView.maximumZoomScale = 2.0
self.scrollView.zoomScale = 1.0
self.scrollView.minimumZoomScale = 0.5
self.pageControl.numberOfPages = self.pageImages.count
self.pageControl.currentPage = self.firstToShow
for _ in 0..<self.pageImages.count {
self.pageViews.append(nil)
}
/* The scroll view, as before, needs to know its content size.
Since you want a horizontal paging scroll view, you calculate the width to be the number of pages multiplied by the width of the scroll view.
The height of the content is the same as the height of the scroll view
*/
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(self.pageImages.count),
height: pagesScrollViewSize.height)
self.scrollView.contentOffset = CGPoint(x: pagesScrollViewSize.width * CGFloat(self.firstToShow), y: 0.0)
// You’re going to need some pages shown initially, so you call loadVisiblePages()
self.loadVisiblePages()
}
The line you highlighted is just rounding down to the closest image index to determine what page the scroll view is on. The problem is when your view controller is displayed no scrolling has been done so it will always show the first image. To get the image you want to show first, you can just calculate what the offset should be for the image set your scroll view's offset to that as shown above.
I have a dynamic cell with autolayout that has an image and a dynamic label. Currently I try to draw the bubble image but I need to know what is the size of the label given its text. I have to say that I always receive a bad size and when I scroll the tableview, the bubble image is placed corectlly but it ads another bubble.
Here is my code:
func imageCellAtIndexPath(indexPath:NSIndexPath) -> SenderTableViewCell{
let cell = self.tableView.dequeueReusableCellWithIdentifier("SenderIdentifier") as! SenderTableViewCell
cell.senderMessageLAbel.text = "fadfasdfasdfasdfasdfasdadfasdfsb"
cell.senderMessageLAbel.sizeToFit()
cell.senderNameLabel.text = "Stefan"
let padding: CGFloat = 10.0
// 4. Calculation of new width and height of the chat bubble view
var viewHeight: CGFloat = 0.0
var viewWidth: CGFloat = 0.0
viewHeight = cell.senderMessageLAbel.frame.size.height + padding
viewWidth = cell.senderMessageLAbel.frame.size.width + padding/2
if viewHeight > viewWidth {
viewHeight = cell.senderMessageLAbel.frame.size.width + padding/2
viewWidth = cell.senderMessageLAbel.frame.size.height + padding
}
let bubbleImageFileName = "bubbleMine"
let imageViewBG : UIImageView = UIImageView(frame: CGRectMake(cell.senderMessageLAbel.frame.origin.x - 5, cell.senderMessageLAbel.frame.origin.y - 10,viewWidth,viewHeight ))
imageViewBG.image = UIImage(named: bubbleImageFileName)?.resizableImageWithCapInsets(UIEdgeInsetsMake(14, 22,17 , 20))
cell.insertSubview(imageViewBG, atIndex: 0)
imageViewBG.center = cell.senderMessageLAbel.center
return cell
}
I can't figure out what is wrong with my code. Any suggestions?
I'm assuming your function is being called at cellForItemAtIndexPath function of UICollectionView DataSource. That function is called multiple times, whenever there is a need of drawing collection view cells that come into view. So each time that happens, you're inserting imageViewBG to the cell, and hence the duplicates.
A quick solution would be to add a specific tag to the imageViewBG as below and remove it each time before re-adding it.
cell.viewWithTag(99)?.removeFromSuperview()
let imageViewBG = //Configure
imageViewBG.tag = 99
cell.insertSubview(imageViewBG, atIndex: 0)
Although this would help solve the duplicates problem, I would strongly suggest you to add the imageView to your custom cell on the storyboard and configure the image that it should display in cellForItemAtIndexPath .