How Do I Add a Parallax Effect To A UITableView Cell - swift

Hello I am creating a sort of social media app and I am using table view to display all the information, I have made a parallax effect and at first it works nicely and smoothly, but after couple of times of moving my device it becomes supper jittery and way to quick
Here is the extension that I have created that I am adding to my TableViewCellView: (I call it like this: myCell.addParallax(magnitude: 10) )
extension UIView {
func addParallax(magnitude: Float) {
let xMotion = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
xMotion.maximumRelativeValue = magnitude
xMotion.minimumRelativeValue = -magnitude
let yMotion = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
yMotion.maximumRelativeValue = magnitude
yMotion.minimumRelativeValue = -magnitude
let motionEffect = UIMotionEffectGroup()
motionEffect.motionEffects = [xMotion, yMotion]
self.addMotionEffect(motionEffect)
}
}

Your UITableViewCells are being reused while the table is scrolled by the user. In the cellForRow atIndexPath method you're most likely using tableView.dequeueReusableCell method. This method doesn't create new cells all the time, it reuses the ones that are not visible anymore.
The UIMotionEffectGroup you're creating is being added to the UITableViewCell each time the myCell.addParallax(magnitude: 10) is called. In the end, you're having many UIMotionEffectGroup which is the source of your problem. Make sure that this method is called only once per cell, you can use awakeFromNib method in the cell itself to achieve this.

Related

Tracking the position of a NSCell on change

I have a NSTableView and want to track the position of its containing NSCells when the tableView got scrolled by the user.
I couldn’t find anything helpful. Would be great if someone can lead me into the right direction!
EDIT:
Thanks to #Ken Thomases and #Code Different, I just realized that I am using a view-based tableView, using tableView(_ tableView:viewFor tableColumn:row:), which returns a NSView.
However, that NSView is essentially a NSCell.
let cell = myTableView.make(withIdentifier: "customCell", owner: self) as! MyCustomTableCellView // NSTableCellView
So I really hope my initial question wasn’t misleading. I am still searching for a way how to track the position of the individual cells/views.
I set the behaviour of the NSScrollView (which contains the tableView) to Copy on Scroll in IB.
But when I check the x and y of the view/cells frame (within viewWillDraw of my MyCustomTableCellView subclass) it remains 0, 0.
NSScrollView doesn't use delegate. It uses the notification center to inform an observer that a change has taken place. The solution below assume vertical scrolling.
override func viewDidLoad() {
super.viewDidLoad()
// Observe the notification that the scroll view sends out whenever it finishes a scroll
let notificationName = NSNotification.Name.NSScrollViewDidLiveScroll
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(_:)), name: notificationName, object: scrollView)
// Post an intial notification to so the user doesn't have to start scrolling to see the effect
scrollViewDidScroll(Notification(name: notificationName, object: scrollView, userInfo: nil))
}
// Whenever the scroll view finished scrolling, we will start coloring the rows
// based on how much they are visible in the scroll view. The idea is we will
// perform hit testing every n-pixel in the scroll view to see what table row
// lies there and change its color accordingly
func scrollViewDidScroll(_ notification: Notification) {
// The data's part of a table view begins with at the bottom of the table's header
let topEdge = tableView.headerView!.frame.height
let bottomEdge = scrollView.bounds.height
// We are going to do hit-testing every 10 pixel. For best efficiency, set
// the value to your typical row's height
let step = CGFloat(10.0)
for y in stride(from: topEdge, to: bottomEdge, by: step) {
let point = NSPoint(x: 10, y: y) // the point, in the coordinates of the scrollView
let hitPoint = scrollView.convert(point, to: tableView) // the same point, in the coordinates of the tableView
// The row that lies that the hitPoint
let row = tableView.row(at: hitPoint)
// If there is a row there
if row > -1 {
let rect = tableView.rect(ofRow: row) // the rect that contains row's view
let rowRect = tableView.convert(rect, to: scrollView) // the same rect, in the scrollView's coordinates system
let visibleRect = rowRect.intersection(scrollView.bounds) // the part of the row that visible from the scrollView
let visibility = visibleRect.height / rowRect.height // the percentage of the row that is visible
for column in 0..<tableView.numberOfColumns {
// Now iterate through every column in the row to change their color
if let cellView = tableView.view(atColumn: column, row: row, makeIfNecessary: true) as? NSTableCellView {
let color = cellView.textField?.textColor
// The rows in a typical text-only tableView is 17px tall
// It's hard to spot their grayness so we exaggerate the
// alpha component a bit here:
let alpha = visibility == 1 ? 1 : visibility / 3
cellView.textField?.textColor = color?.withAlphaComponent(alpha)
}
}
}
}
}
Result:
Update based on edited question:
First, just so you're aware, NSTableCellView is not an NSCell nor a subclass of it. When you are using a view-based table, you are not using NSCell for the cell views.
Also, a view's frame is always relative to the bounds of its immediate superview. It's not an absolute position. And the superview of the cell view is not the table view nor the scroll view. Cell views are inside of row views. That's why your cell view's origin is at 0, 0.
You could use NSTableView's frameOfCell(atColumn:row:) to determine where a given cell view is within the table view. I still don't think this is a good approach, though. Please see the last paragraph of my original answer, below:
Original answer:
Table views do not "contain" a bunch of NSCells as you seem to think. Also, NSCells do not have a position. The whole point of NSCell-based compound views is that they're much lighter-weight than an architecture that uses a separate object for each cell.
Usually, there's one NSCell for each table column. When the table view needs to draw the cells within a column, it configures that column's NSCell with the data for one cell and tells it to draw at that cell's position. Then, it configures that same NSCell with the data for the next cell and tells it to draw at the next position. Etc.
To do what you want, you could configure the scroll view to not copy on scroll. Then, the table view will be asked to draw everything whenever it is scrolled. Then, you would implement the tableView(_:willDisplayCell:for:row:) delegate method and apply the alpha value to the cells at the top and bottom edges of the scroll view.
But that's probably not a great approach.
I think you may have better luck by adding floating subviews to the scroll view that are partially transparent, with a gradient from fully opaque to fully transparent in the background color. So, instead of the cells fading out and letting the background show through, you put another view on top which only lets part of the cells show through.
I just solved the issue by myself.
Just set the contents view postsBoundsChangedNotifications to true and added an observer to NotificationCenter for NSViewBoundsDidChange. Works like a charm!

Transitioning from uitableview to new viewcontroller

I'm working on the "settings" portion of my app. When one of the cells is clicked, I need to transition to a new View Controller depending on which cell was clicked.
I have found a ton of answers, but they all seem to not go far enough. I understand how to set up a segue to a new view controller and I understand how to use didSelectRowAt Indexpath to show which cell was clicked. I can figure out the transition, but I can't figure out many different transitions based on which cell was clicked.
Is there a way to do this with dynamic cells or should I be using static?
If you have fixed amount of cells, I'd go for static cells. However if you want to use dynamic cells, you can create something like enum with cases that store row index.
fileprivate enum Row: Int {
case volume = 0
case notification = 1
}
Then in didSelectRowAt delegate method:
let row = Row(rawValue: indexPath.row)!
switch row {
case .volume:
// Navigate somewhere
case .notification:
// Navigate somewhere
}

Add custom recognizer delay

I've disabled delaysContentTouches in my tableview subclass using:
delaysContentTouches = false
subviews.forEach { ($0 as? UIScrollView)?.delaysContentTouches = false }
But in one of my sections, I still want to keep the delay. Is there a way to cancel the delay for certain sections or perhaps I can add a custom recognizer delay to a section?
Sections are not actual objects within a tableView, so my answer to your first question is no. The .delaysContentTouches applies to the entire tableView.
For your second inquiry, I believe that one way it could be possible is through setting a delay for desired cells' scrollView subview. In your tableView(cellForRowAt: indexPath) func, you could have something like this:
if indexPath.section == 3 { //or whatever your desired section is
for view in cell.subviews {
if view is UIScrollView {
let currentView = view as! UIScrollView
currentView.delaysContentTouches = true
}
}
}
This will find the UIScrollView in your cell's subviews in your desired section. It will then set the .delaysContentTouches property accordingly.
I have not personally executed this code, just researched it, so let me know if it works.
Edit
Apparently the UIScrollView in UITableViewCell has been deprecated, so the above method will not work anymore.
My next best suggestion to you is to use a UILongPressGuestureRecognizer. This will not be quite the same thing as delaying the touch, but could have a similar effect in real execution.
You could use it in the same tableView(cellForRowAt: indexPath) func as so:
let press = UILongPressGestureRecognizer(target: self, action: #selector(handlePress))
press.minimumPressDuration = 2.0 //however long you want
cell.addGestureRecognizer(press)
Whatever you are trying to achieve by selecting certain rows of your tableView could be placed in the handlePress func above which would be trigged upon the long press.

TableView Cells / Visual Format Language / Keeps adding same constraints?

Creating UITableViewCells including elements programmatically seems to be working except for the constraints. Or maybe it is. Everytime the cell reappears the same constraints will be added to the element. I'll have 20 of the same constraints added to a uiLabel after scrolling tableView. Here is my sublcass (condensed):
class TimeSlotCell: UITableViewCell {
let lblTime = UILabel()
var viewCons = [String:AnyObject]()
var previousCageView:UIView!
var myConstraints = [NSLayoutConstraint]()
func configureCell(row:Int,cageCount:Int) {
lblTime.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(lblTime)
placeConstraint("H:|-5-[time\(row)]", view:"time\(row)")
placeConstraint("V:|-17-[time\(row)]", view: "time\(row)
}
func placeConstraint(format:String, view:String) {
viewCons[view] = lblTime
let navViewConstraint = NSLayoutConstraint.constraintsWithVisualFormat(format, options: [], metrics: nil, views: viewCons)
myConstraints += navViewConstraint
NSLayoutConstraint.activateConstraints(myConstraints)
}
And in my VC i'm calling the configureCell func within my cellForRowAtIndexPath.
Is there a way to check if an existing constraint exists? Is there a way to make sure only one of my constraints can be added? Or is there a person out there with a much better solution? Thanks in advance.
There is a dataSource for your tableView, when you configure your cell, you send the cell model to your cell. Whenever the dataSource changes, the cell model sent to the cell will change as well. So what you need to do is move most of 'addSubview' to the cell`s initialization. If you have to do cell.contentView.addSubview with the cell model. You have to judge if the cell model you send to the cell is the same as the former one. If it is equal, do nothing, else reset the view config.

UIImageViews in collectionview are not recycled properly

I have a UICollectionview with 4 UIImageviews inside. These are there for showing specific icons on the cells. Unfortunately when I have a cell that populates all 4 image views with images it works correctly. But when a cell should only display less than 4 items, it sometimes ( so not always ) shows additional icons. It seems to be from other (previous) cells. Like its not recycled properly.
Here is my code. I have an outlet that stores all imageviews inside.
#IBOutlet var savings: [UIImageView]!
Next I have a method inside my Cell Viewcontroller that populates according to the given array of icon names.
func setPropositions(propositions: [SupplierProposition])
{
for (index, item) in propositions.enumerate() {
if index < 4 {
savings[index].image = nil
savings[index].setNeedsDisplay()
if let image = UIImage(named: item.iconName!) {
savings[index].image = image
} else {
savings[index].image = nil
}
}
}
}
as you see I tried everything to make sure the imageviews are empty before rendered, but it doesn't work. Anyone that know what I might be doing wrong?
Like its not recycled properly
No. It's like you are not recycling it properly. It is your job, in cellForItemAtIndexPath:, to completely configure this cell. This cell may have been used already, so it may already have image views in it that you placed there earlier; and if the cell for this index path is supposed to have fewer image views, it is your job to remove the ones you don't want.