I am looking to improve my memory usage on my app and noticed that using AdMob is adversely affecting my application.
Specifically, with this code:
override func viewDidLoad() {
super.viewDidLoad()
multipleAdsOptions.numberOfAds = 5
let imageAdOptions = GADNativeAdImageAdLoaderOptions()
adLoader = GADAdLoader(adUnitID: "ca-app-pub-xxxxxxxx", rootViewController: self, adTypes: [GADAdLoaderAdType.native], options: [multipleAdsOptions, imageAdOptions])
adLoader.delegate = self
GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ kGADSimulatorID.description() ]
DispatchQueue.main.async {
self.adLoader.load(GADRequest())
}
..
..
}
I noticed that each time I trigger a viewDidLoad in my application for this code, it increases the memory used by about 10-15mb. If I quickly tap in and out of this screen (which is actually a common action) I can get the MB usage up as high as I want until I stop. I noticed that this line of code is of course the culprit:
self.adLoader.load(GADRequest())
If I comment that out, my app stays at a clean 50mb and even appears to run much better overall.
Am I using AdMob wrong here? should I be loading the ads once and never calling load again in my viewdidload? This is a highly trafficked screen and my test users are reporting app sluggishness until they restart.
This is my main screen (root view controller) and it contains a feed of products and I also have an AdvertisementFeedCell where I load an ad every 5 items in this feed if that makes sense.
class AdvertisementFeedCell: UITableViewCell {
#IBOutlet var adMedia: GADNativeAdView!
#IBOutlet var adHeadline: UILabel!
#IBOutlet var unifiedNativeAdView: GADNativeAdView!
override func awakeFromNib() {
super.awakeFromNib()
}
}
When I populate my table, I insert a new item every 5 spots for an Ad:
let count = self.feedItems.count / 5
//Insert an ad every 5
if (count > 0)
{
for i in 1...count
{
let adFeedItem = FeedItem()
adFeedItem.isAdvertisement = true
self.feedItems.insert(adFeedItem, at: i * 5)
}
}
self.reloadTable()
And when my tableview populates with data, I check if the item is of typeadvertisement
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let feedItem = feedItems[indexPath.row]
if (feedItem.isAdvertisement)
{
let cell:AdvertisementFeedCell = tableView.dequeueReusableCell(withIdentifier: "adCell")! as! AdvertisementFeedCell
if (ads.count > 0 && indexPath.row > 1)
{
let nativeAd = ads[(indexPath.row / 5) - 1]
cell.unifiedNativeAdView.nativeAd = nativeAd
cell.adHeadline.text = nativeAd.headline!
//If there is an icon, use that
if (nativeAd.icon != nil)
{
(cell.unifiedNativeAdView.iconView as? UIImageView)?.image = nativeAd.icon?.image
cell.unifiedNativeAdView.mediaView?.isHidden = true
cell.unifiedNativeAdView.iconView?.isHidden = false
}
else {
//Otherwise, Add Media and hide icon
cell.unifiedNativeAdView.mediaView?.contentMode = UIViewContentMode.scaleToFill
cell.unifiedNativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
cell.unifiedNativeAdView.mediaView?.isHidden = false
cell.unifiedNativeAdView.iconView?.isHidden = true
}
cell.frame = CGRect(x: 0, y: 0, width: 375, height: 108 )
//Add body text if available
(cell.unifiedNativeAdView.bodyView as? UILabel)?.text = nativeAd.body
cell.unifiedNativeAdView.bodyView?.isHidden = nativeAd.body == nil
cell.clipsToBounds = true
cell.unifiedNativeAdView.clipsToBounds = true
}
return cell
}
Thank you!
Related
I'm writing a demo to show user's tweets.
The question is:
Every time I scroll to the bottom and then scroll back, the tweet's images and comments are reloaded, even the style became mess up. I know it something do with dequeue, I set Images(which is an array of UIImageView) to [] every time after dequeue, but it is not working. I'm confused and couldn't quite sleep....
Here is core code of my TableCell(property and Images set), which provide layout:
class WechatMomentListCell: UITableViewCell{
static let identifier = "WechatMomentListCell"
var content = UILabel()
var senderAvatar = UIImageView()
var senderNick = UILabel()
var images = [UIImageView()]
var comments = [UILabel()]
override func layoutSubviews() {
//there is part of Image set and comments
if images.count != 0 {
switch images.count{
case 1:
contentView.addSubview(images[0])
images[0].snp.makeConstraints{ (make) in
make.leading.equalTo(senderNick.snp.leading)
make.top.equalTo(content.snp.bottom)
make.width.equalTo(180)
make.height.equalTo(180)
}
default:
for index in 0...images.count-1 {
contentView.addSubview(images[index])
images[index].snp.makeConstraints{ (make) in
make.leading.equalTo(senderNick.snp.leading).inset(((index-1)%3)*109)
make.top.equalTo(content.snp.bottom).offset(((index-1)/3)*109)
make.width.equalTo(90)
make.height.equalTo(90)
}
}
}
}
if comments.count != 0, comments.count != 1 {
for index in 1...comments.count-1 {
comments[index].backgroundColor = UIColor.gray
contentView.addSubview(comments[index])
comments[index].snp.makeConstraints{(make) in
make.leading.equalTo(senderNick)
make.bottom.equalToSuperview().inset(index*20)
make.width.equalTo(318)
make.height.equalTo(20)
}
}
}
}
Here is my ViewController, which provide datasource:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tweetCell = tableView.dequeueReusableCell(withIdentifier: WechatMomentListCell.identifier, for: indexPath) as? WechatMomentListCell else {
fatalError("there is no WechatMomentList")
}
let tweet = viewModel.tweetList?[indexPath.row]
for i in tweet?.images ?? [] {
let flagImage = UIImageView()
flagImage.sd_setImage(with: URL(string: i.url))
tweetCell.images.append(flagImage)
}
for i in tweet?.comments ?? [] {
let flagComment = UILabel()
flagComment.text = "\(i.sender.nick) : \(i.content)"
tweetCell.comments.append(flagComment)
}
return tweetCell
}
The Images GET request has been define at ViewModel using Alamofire.
The firsttime is correct. However, If I scroll the screen, the comments will load again and images were mess up like this.
I found the problem in your tableview cell. in cell you have two variables like this.
var images = [UIImageView()]
var comments = [UILabel()]
Every time you using this cell images and comments are getting appended. make sure you reset these arrays every time you use this cell. like setting theme empty at initialization.
there is a weird issue about UITableView. I have 3 data source it means 3 section in UITableView. When I scroll the UITableView, button and images are conflicting. Button is disappearing, images are becoming deformed.
Here my cellForRowAt method.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "eventnetworkingcell", for: indexPath) as! EventNetworkCell
var name: String = ""
var job: String = ""
var company: String = ""
var img: Int?
cell.userImageView.layer.cornerRadius = 45
cell.userImageView.clipsToBounds = true
if indexPath.section == 0 {
name = self.matchmaking[indexPath.row].name
job = self.matchmaking[indexPath.row].job
company = self.matchmaking[indexPath.row].company
img = self.matchmaking[indexPath.row].image
}
else if indexPath.section == 1 {
name = self.networkInEvent[indexPath.row].name
job = self.networkInEvent[indexPath.row].job
company = self.networkInEvent[indexPath.row].company
img = self.networkInEvent[indexPath.row].image
cell.addButtonOutlet.alpha = 0
}
else {
name = self.allAttendees[indexPath.row].name
job = self.allAttendees[indexPath.row].job
company = self.allAttendees[indexPath.row].company
img = self.allAttendees[indexPath.row].image
}
cell.nameLabel.text = name
cell.jobLabel.text = job
cell.companyLabel.text = company
if let imgid = img {
let url = MTApi.url(for: imgid, size: .normal)
cell.userImageView.sd_setImage(with: url, placeholderImage: nil, options: [], completed: nil)
}
}
cell.addButtonOutlet.addTarget(self, action:
#selector(self.addNetwork(sender:)), for: .touchUpInside)
return cell
}
When I remove the cell.addButtonOutlet.alpha = 0 line, buttons aren't disappearing.
And there is a video which shows the issue:
Video
You're having an issue with cells reusing.
Basically, what's happening, is that the tableView creates only enough cells as much as there are visible on the screen, and once a cell gets scrolled out of view, it gets reused for another item in your dataSource.
The reason for buttons seemingly disappearing is that that you've already removed the button previously, but now, when reusing haven't told the cell to show the button again.
Fixing this is easy, just add:
cell.addButtonOutlet.alpha = 0
to your sections 0 and 2 (else block).
Same thing with images, the previous image is retained unless you tell the cell to remove the image if needed, so just add this:
if let imgid = img {
let url = MTApi.url(for: imgid, size: .normal)
cell.userImageView.sd_setImage(with: url, placeholderImage: nil, options: [], completed: nil)
} else {
cell.userImageView.image = nil
}
var name: String = ""
var job: String = ""
var company: String = ""
var img: Int?
cell.userImageView.layer.cornerRadius = 45
cell.userImageView.clipsToBounds = true
cell.addButtonOutlet.alpha = 1 // add more
if indexPath.section == 0 {
........
and you should research about UITableviewCell reuse.
I have a problem when I use collection view with segmentView.
I have a viewController which has a segmentView with 4 elements ( 0 => All , 1 => Photos , 2 => Audios , 3 => videos )
here's and example :
And I have one collectionView to display the data depend on which category clicked from the segmentController
It's working and displaying the data and here's my collectionView methods to display data
// MARK: Datasource collection method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// All Items
if segmentStatues == 0
{
let cellForAll = collectionView.dequeueReusableCell(withReuseIdentifier: "AllItemsCell", for: indexPath) as! AllItemsCollectionViewCell
// reset elements before declare
cellForAll.cellImage.image = nil
cellForAll.playBtn.isHidden = true
cellForAll.layer.borderWidth = 1
cellForAll.layer.borderColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0).cgColor
// When type is an image
if sociaPosts[(indexPath as NSIndexPath).row].postType == "image"
{
cellForAll.cellImage.sd_setImage(with: URL(string: sociaPosts[(indexPath as NSIndexPath).row].postUrl))
}else if sociaPosts[(indexPath as NSIndexPath).row].postType == "audio"
{
cellForAll.cellImage.image = UIImage(named: "recorde_icon")
}else if sociaPosts[(indexPath as NSIndexPath).row].postType == "video"
{
cellForAll.cellImage.sd_setImage(with: URL(string: sociaPosts[(indexPath as NSIndexPath).row].thumbURL))
cellForAll.playBtn.isHidden = false
}
return cellForAll
}else{
// Cell if Images or videos or audios
let newCell = collectionView.dequeueReusableCell(withReuseIdentifier: "beepbeep", for: indexPath) as! userProfileImageCollectionViewCell
// If the type is images
if segmentStatues == 1
{
// Set image URL
newCell.cellImage.sd_setImage(with: URL(string: imagesPosts[(indexPath as NSIndexPath).row].postUrl))
// Get image likes and comments and shares
newCell.likeCount.text = String(imagesPosts[(indexPath as NSIndexPath).row].likes_count)
newCell.commentCount.text = String(imagesPosts[(indexPath as NSIndexPath).row].comments_count)
newCell.shareCount.text = String(imagesPosts[(indexPath as NSIndexPath).row].shares_count)
} else if segmentStatues == 3
{
// For Video player
var player = newCell.customVid
player = Bundle.main.loadNibNamed("VieoPlayer", owner: self, options: nil)?.last as? videoPlayer
player?.newIntitVideoPlayer(forViewController: self, videoURL: videosPosts[indexPath.row].postUrl , videoThumbnail: URL(string: videosPosts[indexPath.row].thumbURL), onReady: nil)
// Set video URL and Thumbnail
// Get video likes and comments and shares
newCell.likeCount.text = String(videosPosts[(indexPath as NSIndexPath).row].likes_count)
newCell.commentCount.text = String(videosPosts[(indexPath as NSIndexPath).row].comments_count)
newCell.shareCount.text = String(videosPosts[(indexPath as NSIndexPath).row].shares_count)
}else if segmentStatues == 2
{
// Audio player
let ad = Bundle.main.loadNibNamed("AudioPlayerView", owner: self, options: nil)?.last as! AudioPlayerView
ad.frame = CGRect(x: (newCell.contentView.frame.size.width - newCell.contentView.frame.size.height * 0.6) / 2, y: 20 , width: newCell.contentView.frame.size.height * 0.6, height: newCell.contentView.frame.size.height * 0.6)
ad.playImageView.image = nil
ad.tag = 6666
newCell.contentView.addSubview(ad)
// Set audio URL
ad.audioPath = URL(string: audiosPosts[indexPath.row].postUrl)
ad.viewInitialization()
// Get Audio likes and comments and shares
newCell.likeCount.text = String(audiosPosts[(indexPath as NSIndexPath).row].likes_count)
newCell.commentCount.text = String(audiosPosts[(indexPath as NSIndexPath).row].comments_count)
newCell.shareCount.text = String(audiosPosts[(indexPath as NSIndexPath).row].shares_count)
}
return newCell
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.segmentStatues == 0
{
return sociaPosts.count
}else if self.segmentStatues == 1
{
return imagesPosts.count
}else if self.segmentStatues == 2
{
return audiosPosts.count
}else
{
return videosPosts.count
}
}
I have 2 custom cells in my collection view one for all items and the other for (images - audios - videos ) view
my problem is data is displaying but cells duplicated when i change between segment in userProfileImageCollectionViewCell
these 2 pictures show the issue
Displaying all audios cell when i click in audio segmnet
But after I click in image or video segment cell duplicated like this
it's happen in audio - images - videos only**(userProfileImageCollectionViewCell)**
and finally here's my userProfileImageCollectionViewCell code :
updateded :
import UIKit
import SDWebImage
class userProfileImageCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var customVid: videoPlayer!
#IBOutlet weak var cellImage: UIImageView!
#IBOutlet weak var likeButton: UIButton!
#IBOutlet weak var likeCount: UILabel!
#IBOutlet weak var commentButton: UIButton!
#IBOutlet weak var commentCount: UILabel!
#IBOutlet weak var shareButton: UIButton!
#IBOutlet weak var shareCount: UILabel!
override func prepareForReuse() {
cellImage.sd_setImage(with: nil)
self.customVid.subviews.forEach({ $0.removeFromSuperview() })
}
}
The issue is that UICollectionViewCell (as well as UITableViewCell by the way) are reused.
To put it simply :
When a cell disappear from screen when you scroll, it may come back at the top/bottom (according to the scroll direction). But it's not a new "clean" cell, it's the previous one. That's the reuse system.
So, when you did: newCell.contentView.addSubview(ad) you added each times a new subview without checking if there was one already. The subview where juste piling. It's easy to check it with the 3D Hierarchy Debug of XCode.
Since you have a userProfileImageCollectionViewCell file, the best solution is to use prepareForReuse().
Create a IBOutlet (let's call it customVid) that will add as its subview the ad/player.
In prepareForReuse() remove all the subview of customVid.
In collectionView:cellForRowAtIndexPath:, do something like that:
var player = Bundle.main.loadNibNamed("VieoPlayer", owner: self, options: nil)?.last as? videoPlayer
player?.newIntitVideoPlayer(forViewController: self, videoURL: videosPosts
newCell.customVid.addSubview(player)
Replicate the same mechanism for the ad (you can use the same subview, it's up to you).
Now, a few suggestions not related to your issue:
Name your class starting with an uppercase: userProfileImageCollectionViewCell => UserProfileImageCollectionViewCell
I'd create methods in userProfileImageCollectionViewCell to make your code more readable. I don't speak Swift, so my next lines of code may not compile, but you should get the idea:
func fillWithVideo(videoParam video: CustomVideoClass) {
var player = newCell.customVid
player = Bundle.main.loadNibNamed("VieoPlayer", owner: self, options: nil)?.last as? videoPlayer
player?.newIntitVideoPlayer(forViewController: NOTself, videoURL: video.postUrl, videoThumbnail: URL(string: video.thumbURL), onReady: nil)
self.customVid.addSubview(player)
// Set video URL and Thumbnail
// Get video likes and comments and shares
self.likeCount.text = String(video.likes_count)
self.commentCount.text = String(video.comments_count)
self.shareCount.text = String(video.shares_count)
}
In collectionView:cellForRowAtIndexPath:
//If it's the correct section
let video = videosPosts[(indexPath as NSIndexPath).row] as CustomVideoClass
cell fillWithVideo(video)
Note that it seems that you use the UIViewController, so you may want to add the UIViewController parameter in order to get the newIntitVideoPlayer() method to work, but I didn't want to put it in the previous example to keep it more simple. You may need to replace the lines:
cell fillWithVideo(video andViewController:self)
and
func fillWithVideo(videoParam video: CustomVideoClass andViewController viewController: UIViewController)
player?.newIntitVideoPlayer(forViewController: viewController, videoURL: video.postUrl, videoThumbnail: URL(string: video.thumbURL), onReady: nil)
Etc. for each part specific part. It make just easier to read what you are doing. Handle only in collectionView:cellForRowAtIndexPath: the logic (which section should get which item, etc.) and the rest to the cell that may adapt.
I'm trying to implement a smoother way for a use to add new collectionViewCells for myCollectionView (which only shows one cell at a time). I want it to be something like when the user swipes left and if the user is on the last cell myCollectionView inserts a new cell when the user is swiping so that the user swipes left "into" the cell. And also I only allow the user to scroll one cell at a time.
EDIT:
So I think it is a bit hard to describe it in words so here is a gif to show what I mean
So in the past few weeks I have been trying to implement this in a number of different ways, and the one that I have found the most success with is by using the scrollViewWillEndDragging delegate method and I have implemented it like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Getting the size of the cells
let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width
let cellPadding = 10.0 as! CGFloat
// Calculating which page "card" we should be on
let currentOffset = scrollView.contentOffset.x - cellWidth/2
print("currentOffset is \(currentOffset)")
let cardWidth = cellWidth + cellPadding
var page = Int(round((currentOffset)/(cardWidth) + 1))
print("current page number is: \(page)")
if (velocity.x < 0) {
page -= 1
}
if (velocity.x > 0) {
page += 1
}
print("Updated page number is: \(page)")
print("Previous page number is: \(self.previousPage)")
// Only allowing the user to scroll for one page!
if(page > self.previousPage) {
page = self.previousPage + 1
self.previousPage = page
}
else if (page == self.previousPage) {
page = self.previousPage
}
else {
page = self.previousPage - 1
self.previousPage = page
}
print("new page number is: " + String(page))
print("addedCards.count + 1 is: " + String(addedCards.count + 1))
if (page == addedCards.count) {
print("reloading data")
// Update data source
addedCards.append("card")
// Method 1
cardCollectionView.reloadData()
// Method 2
// let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
// cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
}
// print("Centering on new cell")
// Center the cardCollectionView on the new page
let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
print("newOffset is: \(newOffset)")
targetContentOffset.memory.x = newOffset
}
Although I think I have nearly got the desired result there are still some concerns and also bugs that I have found.
My main concern is the fact that instead of inserting a single cell at the end of myCollectionView I'm reloading the whole table. The reason that I do this is because if I didn't then myCollectionView.contentOffset wouldn't be changed and as a result when a new cell is created, myCollectionView isn't centered on the newly created cell.
1. If the user scrolls very slowly and then stops, the new cell gets created but then myCollectionView gets stuck in between two cells it doesn't center in on the newly created cell.
2. When myCollectionView is in between the second last cell and the last cell as a result of 1., the next time the user swipes right, instead of creating one single cell, two cells are created.
I've also used different ways to implement this behaviour such as using scrollViewDidScroll, and various other but to no avail. Can anyone point me in the right direction as I am kind of lost.
Here is a link to download my project if you want to see the interaction:
My Example Project
These the old methods if you're interested:
To do this I have tried 2 ways,
The first being:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// page is the current cell that the user is on
// addedCards is the array that the data source works with
if (page == addedCards.count + 1) {
let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
addedCards.append(placeholderFlashCardProxy)
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
cardCollectionView.reloadData()
}
}
The problem with using this method is that:
Sometimes I will get a crash as a result of: NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.
When a new collectionCell is added it will sometimes display the input that the user has written from the previous cell (this might have sometimes to do with the dequeuing and re-use of cell, although again, I'm not sure and I would be grateful if someone answered it)
The inserting isn't smooth, I want the user to be able to swipe left at the last cell and "into" a new cell. As in if I am currently on the last cell, swiping left would put me automatically at the new cell, because right now when I swipe left a new cell is created by it doesn't center on the newly created cell
The second method that I am using is:
let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self
Although the swipe gesture very rarely responds, but if I use a tap gesture, myCollectionView always responds which is very weird (I know again this is a question on its own)
My question is which is the better way to implement what I have described above? And if none are good what should I work with to create the desired results, I've been trying to do this for two days now and I was wondering if someone could point me in the right direction. Thanks!
I hope this helps out in some way :)
UPDATE
I updated the code to fix the issue scrolling in either direction.
The updated gist can be found here
New Updated Gist
Old Gist
First I'm gonna define some model to for a Card
class Card {
var someCardData : String?
}
Next, create a collection view cell, with a card view inside that we will apply the transform to
class CollectionViewCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
self.addSubview(cardlabel)
}
override func prepareForReuse() {
super.prepareForReuse()
cardView.alpha = 1.0
cardView.layer.transform = CATransform3DIdentity
}
override func layoutSubviews() {
super.layoutSubviews()
cardView.frame = CGRectMake(contentPadding,
contentPadding,
contentView.bounds.width - (contentPadding * 2.0),
contentView.bounds.height - (contentPadding * 2.0))
cardlabel.frame = cardView.frame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var cardView : UIView = {
[unowned self] in
var view = UIView(frame: CGRectZero)
view.backgroundColor = UIColor.whiteColor()
return view
}()
lazy var cardlabel : UILabel = {
[unowned self] in
var label = UILabel(frame: CGRectZero)
label.backgroundColor = UIColor.whiteColor()
label.textAlignment = .Center
return label
}()
}
Next setup the view controller with a collection view. As you will see there is a CustomCollectionView class, which I will define near the end.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var cards = [Card(), Card()]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding)
}
lazy var collectionView : CollectionView = {
[unowned self] in
// MARK: Custom Flow Layout defined below
var layout = CustomCollectionViewFlowLayout()
layout.contentDelegate = self
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
collectionView.clipsToBounds = true
collectionView.showsVerticalScrollIndicator = false
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cards.count
}
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize {
return CGSizeMake(collectionView.bounds.width, tableViewHeight)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cellIdentifier = "CollectionViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell
cell.contentView.backgroundColor = UIColor.blueColor()
// UPDATE If the cell is not the initial index, and is equal the to animating index
// Prepare it's initial state
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{
cell.cardView.alpha = 0.0
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0)
}
return cell
}
}
UPDATED - Now For the really tricky part. I'm gonna define the CustomCollectionViewFlowLayout. The protocol callback returns the next insert index calculated by the flow layout
protocol CollectionViewFlowLayoutDelegate : class {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath)
}
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var contentDelegate: CollectionViewFlowLayoutDelegate?
// Tracks the card to be animated
// TODO: - Adjusted if cards are deleted by one if cards are deleted
private var animatingIndex : Int = 0
// Tracks thje currently visible index
private var visibleIndex : Int = 0 {
didSet {
if visibleIndex > oldValue {
if visibleIndex > animatingIndex {
// Only increment the animating index forward
animatingIndex = visibleIndex
}
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 {
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0)
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex)
}
} else if visibleIndex < oldValue && animatingIndex == oldValue {
// if we start panning to the left, and the animating index is the old value
// let set the animating index to the last card.
animatingIndex = oldValue + 1
}
}
}
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enought in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}
And define the delegate methods in your view controller to insert a new card when the delegate tell it that it needs a new index
extension ViewController : CollectionViewFlowLayoutDelegate {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
cards.append(Card())
collectionView.performBatchUpdates({
self.collectionView.insertItemsAtIndexPaths([index])
}) { (complete) in
}
}
And below is the custom Collection view that applies the animation while scrolling accordingly :)
class CollectionView : UICollectionView {
override var contentOffset: CGPoint {
didSet {
if self.tracking {
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go
// So you should be adjusting the second to last cell on the screen
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0))
} else {
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index
// by adding 1, thus we need to continue the trasition on the second the last cell
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0))
}
}
}
/**
This method applies the transform accordingly to the cell at a specified index
- parameter atIndex: index of the cell to adjust
*/
func adjustTransitionForOffset(atIndex : NSIndexPath) {
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell {
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width
lastCell.cardView.alpha = progress
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0)
}
}
}
I think u missed one point here in
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
the indexPath should be
NSIndexPath(forItem : addedCards.count - 1, inSection : 0) not addedCards.count
Thats why you were getting the error
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0
I'd like to use an instance of a separate class as my UITableView's datasource & delegate. I create a new instance of the class that should handle this role and assign it via my tableview's properties:
autoMakesTable is defined as property:
var autoMakesTable : UITableView?
Further down:
func nextBttnTouched(sender: AnyObject) {
switch self.autoState {
case .NewOrEdit:
self.autoState = .YearEntered
// If the picker wasn't used create a new Auto with the default year value
self.auto = Auto(year: NSNumber(integerLiteral: self.yearPickerValues.count - 2), make: "", model: "", body: "", trim: "", downPaymentBudget: 0, monthlyPaymentBudget: 0)
// Prepare the make & model fields
backgroundFieldView = UIView(frame: CGRectMake(0, 0-self.view.frame.size.height/2, self.view.frame.width, self.view.frame.height/2))
backgroundFieldView!.backgroundColor = UIColor(red: 251/255, green: 251/255, blue: 251/255, alpha: 1.0)
let makeField = UITextField(frame: CGRectMake(self.view.center.x - 100, self.navigationController!.navigationBar.frame.size.height + 34, 200, 30))
makeField.delegate = self
makeField.placeholder = "Make"
makeField.addTarget(self, action: Selector("makeTextChanged:"), forControlEvents: UIControlEvents.EditingChanged)
let modelField = UITextField(frame: CGRectMake(self.view.center.x - 100, makeField.frame.origin.y + makeField.frame.size.height + 8, 200, 30))
modelField.placeholder = "Model"
let newAutoMakesTableData = AutoMakesTableData()
autoMakesTable = UITableView(frame: CGRectMake(makeField.frame.origin.x, makeField.frame.origin.y + makeField.frame.height + 2, makeField.frame.width, 100))
autoMakesTable!.dataSource = newAutoMakesTableData
autoMakesTable!.delegate = newAutoMakesTableData
self.backgroundFieldView?.addSubview(self.autoMakesTable!)
self.autoMakesTable!.reloadData()
makeField.becomeFirstResponder()
self.addBorderToTextField([makeField,modelField])
self.view.addSubview(backgroundFieldView!)
// Move year picker with animation
// Update next button location while loading make / model input fields with animation
UIView.animateWithDuration(0.8, animations: { () -> Void in
self.newYearPicker?.frame.origin.y = 0
self.backgroundFieldView!.frame.origin.y = 0
self.backgroundFieldView!.addSubview(makeField)
self.backgroundFieldView?.addSubview(modelField)
//self.backgroundFieldView?.addSubview(self.autoMakesTable!)
// self.autoMakesTable!.reloadData()
})
case .YearEntered:
self.autoState = .MakeModelEntered
case .MakeModelEntered:
self.autoState = .DetailsEntered
case .DetailsEntered:
self.autoState = .BudgetEntered
case .BudgetEntered:
performSegueWithIdentifier("next", sender: self)
}
}
It's added onto the view hierarchy (I intact see an empty tableview with a few rows) later on. I also try to call reloadData() on it to no effect.
AutoMakesTableData is defined like so:
import UIKit
class AutoMakesTableData: NSObject, UITableViewDataSource, UITableViewDelegate {
// Auto makes URL
// This should be hosted online so changes can be made w/out having to re-submit the app
let urlForMakes = NSBundle.mainBundle().URLForResource("makes", withExtension: "JSON")
var autoMakes : [String]?
override init() {
super.init()
self.populateAutoMakesFromURL(urlForMakes!)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let aCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
print("cell created")
return aCell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("selected row")
}
private func populateAutoMakesFromURL(url: NSURL) {
do {
let jsonAutoMakesData = try NSData(contentsOfURL: url, options: NSDataReadingOptions.DataReadingMappedIfSafe)
do {
let jsonAutoMakes = try NSJSONSerialization.JSONObjectWithData(jsonAutoMakesData, options: NSJSONReadingOptions.MutableContainers) as! [String:[String]]
self.autoMakes = jsonAutoMakes["makes"]
// Sort array alphabetically
self.autoMakes?.sortInPlace(<)
} catch let error as NSError {
print(error.userInfo.debugDescription)
}
} catch let error as NSError {
print(error.userInfo.debugDescription)
}
}
}
I can access the autoMakes array property on this instance without a problem.
The table view shows up but none of the print statements fire (for a cell at index path or for did select row).
I don't see anywhere where you install the table view into the view hierarchy. If you don't install the view controller into the active view hierarchy it won't do anything.
That is my guess as to what's going wrong.
It's ok to have a separate object serve as the data source/delegate of the view controller.
EDIT:
Based on discussion below this answer, it turns out the problem was that the OP was creating his AutoMakesTableData in a method and assigning it to a local variable but not saving any other strong references to it.
When the method that created the AutoMakesTableData object returned, the local variable went out of scope, there were no more strong references to the object, and it was deallocated.
A table view's delegate and dataSource properties are weak, so those get zeroed out when the object is deallocated. The table view no longer has a data source, so nothing happens.