Swift animate view in tableviewcell - ios

For an app that I'm developing, I'm using a tableview. My tableview, has of course a tableviewcell. In that tableviewcell I'm adding a view programmatically (so nothing in storyboard). It's a view where I draw some lines and text and that becomes a messagebubble. If the messagebubble is seen by the other user you sent it too , a line of the bubble will go open.
So I have the animation function inside the class of that UIView (sendbubble.swift)
Now, it already checks if it is read or not and it opens the right bubble. But normally it should animate (the line that goes open should rotate) in 0.6 seconds. But it animates instantly. So my question is, how do I animate it with a duration?
I would also prefer to still call it in my custom UIView class (sendbubble.swift) . Maybe I need code in my function to check if the cell is presented on my iphone?
Thanks in advance!
func openMessage() {
UIView.animate(withDuration: 0.6, delay: 0.0, options: [], animations: {
var t = CATransform3DIdentity;
t = CATransform3DMakeRotation(CGFloat(3 * Float.pi / 4), 0, 0, 1)
self.moveableLineLayer.transform = t;
}, completion:{(finished:Bool) in })
}

First you need to grab the cell.
Get the indexPath of that cell where you need to show open bubble.
Get the cell from tableView.cellForRowAt(at:indexPath)
We will now have access to that bubble view now you can animate using the same function func openMessage()
Any question? comment.

You need to implement the UITableViewDelegate method tableView(_:willDisplay:forRowAt:) https://developer.apple.com/reference/uikit/uitableviewdelegate/1614883-tableview
That is triggered when the cell is about to be drawn - not when it is created. You will also need to store a state in your model that says if the animation has occurred, otherwise it will happen every time the cell comes back into view.
EXAMPLE
In the view controller (pseudo code)
class CustomViewController: UITableViewDelegate {
//where ever you define your tableview
var tableView:UITableView
tableView.delegate = self
var dataSource //some array that is defining your cells - each object has a property call hasAnimated
func tableView(_ tableView: UITableView, willDisplay cell: ITableViewCell,
forRowAt indexPath: IndexPath) {
if let cell = cell as? CustomTableViewCell, dataSource[indexPath.row].hasAnimated == false {
dataSource[indexPath.row].hasAnimated = true
cell.openMessage()
}
}
}
class CustomTableViewCell: UITableViewCell {
func openMessage() { //your method
}
}

Related

Why the CollectionViewCell is malfunctioning when scrolling up and down?

The problem
When scrolling up and down in my (programmatically) created collectionView the cells doesn't seem to dequeued properly. This is resulting in duplication of it contents.
Video
Bug replication
Wished behaviour
I wish that the cells correctly getting dequeued and that the content does not get duplicated.
Code snippet
Code snippets are provided via Pastebin below. I had to add some code to satisfy the markdown editor here on SO...
open class CollectionDataSource<Provider: CollectionDataProviderProtocol, Cell: UICollectionViewCell>: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout where Cell: ConfigurableCell, Provider.T == Cell.T {
https://pastebin.com/CzHYxTDD
class ProductCell: UICollectionViewCell, ConfigurableCell {
}
https://pastebin.com/9Nkr3s4B
If anything else is need, please ask in the comments.
Each time you call
func configure(_ item: ProductViewModel, at indexPath: IndexPath) {
setupProductImage(with: item.productImage)
setupStackView()
setupProductLines(with: item.productLines)
}
You create new instance productLineLabel = UILabel() inside setupProductLines() and add it to the stackView
You should change this behavior or rather clear the stack view in prepareForReuse method.
Keep in mind, that addArrangedSubview increases suviews retain count for newly added elements. If you stop your applications execution using Debug View Hierarchy button (fig 1), most likely you will see more labels than you expect in the cell.
fig 1.
The problem
Each time I call:
func configure(_ item: ProductViewModel, at indexPath: IndexPath) {
setupProductImage(with: item.productImage)
setupStackView()
setupProductLines(with: item.productLines)
}
I create a new instance of productLineLabel = UILabel()
Therefore it will be duplicated each time the configure(_ item:) is being called from the cellForRowAtIndexPath.
The solution
I used prepareForReuse recommended by llb to remove the subviews that were kind of class UIStackview (containing UILabels). I wrote the following extension to make this less tedious:
func addSubviews(with subviews: [UIView], in parent: UIView) {
subviews.forEach { parent.addSubview($0) }
}
The implementation
The only thing what was left to do was calling the custom extension function from prepareForReuse like so:
override func prepareForReuse() {
let foundStackView = subviews.filter({$0.isKind(of: UIStackView.self)})[0] as? UIStackView
guard let labels = foundStackView?.arrangedSubviews.filter({$0.isKind(of: UILabel.self)}) else { return }
foundStackView?.removeArrangedSubviews(labels, shouldRemoveFromSuperview: true)
}
Credits go to llb, see comments below! <3 Thanks.

Need help setting UISwitch in custom cell (XIB, Swift 4, Xcode 9)

Successes so far: I have a remote data source. Data gets pulled dynamically into a View Controller. The data is used to name a .title and .subtitle on each of the reusable custom cells. Also, each custom cell has a UISwitch, which I have been able to get functional for sending out both a “subscribe” signal for push notifications (for a given group identified by the cell’s title/subtitle) and an “unsubscribe” signal as well.
My one remaining issue: Whenever the user "revisits" the settings VC, while my code is "resetting" the UISwitches, it causes the following warnings in Xcode 9.2:
UISwitch.on must be used from main thread
UISwitch.setOn(_:animated:) must be used from main thread only
-[UISwitch setOn:animated:notifyingVisualElement:] must be used from main thread
The code below "works" -- however the desired result happens rather slowly (the UISwitches that are indeed supposed to be "on" take a good while to finally flip to "on").
More details:
What is needed: Whenever the VC is either shown or "re-shown," I need to "reset" the custom cell’s UISwitch to "on" if the user is subscribed to the given group, and to "off" if the user is not subscribed. Ideally, each time the VC is displayed, something should reach out and touch the OneSignal server and find out that user’s “subscribe state” for each group, using the OneSignal.getTags() function. I have that part working. This code is in the VC. But I need to do it the right way, to suit proper protocols regarding threading.
VC file, “ViewController_13_Settings.swift” holds a Table View with the reusable custom cell.
Table View file is named "CustomTableViewCell.swift"
The custom cell is called "customCell" (I know, my names are all really creative).
The custom cell (designed in XIB) has only three items inside it:
Title – A displayed “friendly name” of a “group” to be subscribed to or unsubscribed from. Set from the remote data source
Subtitle – A hidden “database name” of the aforementioned group. Hidden from the user. Set from the remote data source.
UISwitch - named "switchMinistryGroupList"
How do I properly set the UISwitch programmatically?
Here is the code in ViewController_13_Settings.swift that seems pertinent:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
// set cell's title and subtitle
cell.textLabelMinistryGroupList?.text = MinistryGroupArray[indexPath.row]
cell.textHiddenUserTagName?.text = OneSignalUserTagArray[indexPath.row]
// set the custom cell's UISwitch.
OneSignal.getTags({ tags in
print("tags - \(tags!)")
self.OneSignalUserTags = String(describing: tags)
print("OneSignalUserTags, from within the OneSignal func, = \(self.OneSignalUserTags)")
if self.OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
}, onFailure: { error in
print("Error getting tags - \(String(describing: error?.localizedDescription))")
// errorWithDomain - OneSignalError
// code - HTTP error code from the OneSignal server
// userInfo - JSON OneSignal responded with
})
viewWillAppear(true)
return cell
}
}
In the above portion of the VC code, this part (below) is what is functioning but apparently not in a way the uses threading properly:
if OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
It's not entirely clear what your code is doing, but there seems to be a few things that need sorting out, that will help you solve your problem.
1) Improve the naming of your objects. This helps others see what's going on when asking questions.
Don't call your cell CustomTableViewCell - call it, say, MinistryCell or something that represents the data its displaying. Rather than textLabelMinistryGroupList and textHiddenUserTagName tree ministryGroup and userTagName etc.
2) Let the cell populate itself. Make your IBOutlets in your cell private so you can't assign to them directly in your view controller. This is a bad habit!
3) Create an object (Ministry, say) that corresponds to the data you're assigning to the cell. Assign this to the cell and let the cell assign to its Outlets.
4) Never call viewWillAppear, or anything like it! These are called by the system.
You'll end up with something like this:
In your view controller
struct Ministry {
let group: String
let userTag: String
var tagExists: Bool?
}
You should create an array var ministries: [Ministry] and populate it at the start, rather than dealing with MinistryGroupArray and OneSignalUserTagArray separately.
In your cell
class MinistryCell: UITableViewCell {
#IBOutlet private weak var ministryGroup: UILabel!
#IBOutlet private weak var userTagName: UILabel!
#IBOutlet private weak var switch: UISwitch!
var ministry: Ministry? {
didSet {
ministryGroup.text = ministry?.group
userTagName.text = ministry?.userTag
if let tagExists = ministry?.tagExists {
switch.isEnabled = false
switch.isOn = tagExists
} else {
// We don't know the current state - disable the switch?
switch.isEnabled = false
}
}
}
}
Then you dataSource method will look like…
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MinistryCell
let ministry = ministries[indexPath.row]
cell.ministry = ministry
if ministry.tagExists == nil {
OneSignal.getTags { tags in
// Success - so update the corresponding ministry.tagExists
// then reload the cell at this indexPath
}, onFailure: { error in
print("Error")
})
}
return cell
}

Why isn't my label hiding?

I have a tableViewCell with a stackView. I'm trying to make it a collapsable cell. I can successfully set the hidden state of the 2nd label during tableView:cellForRowAt:
However if I change the hidden state in response to a tap of the cell, visibility doesn't change on-screen.
UIView.animate(withDuration: 0.25) { [unowned self] in
self.disclaimers.isHidden = !cellItem.isExpanded
}
The cellItem.isExpanded property is toggled properly as is the label .isHidden property. Just no change on-screen.
I know this is already on the main thread as tableView:didSelectRowAt: is called on the main thread. But to cover myself I tried wrapping the line with a DispatchQueue.main.async { } call instead of the UIView.animate. No change.
Any ideas?
Full project is here if you're willing: https://github.com/AaronBratcher/TableViewTester
Issue is in DisclaimersCell.swift. Line 33
Couple issues...
You should never call .dequeueReusableCell anywhere other than in cellForRowAt. You do that in TableViewController.swift -> didSelectRowAt ... which creates a new instance of the cell, not the existing instance in your table. Change your guard line to:
guard var cellItem = helper.cellForRowAtIndexPath(indexPath) as? TableViewExpandableCellItem
, let cell = tableView.cellForRow(at: indexPath) as? TableViewExpandableCell
, cellItem.shouldExpand
else { return }
That way, tableView.cellForRow(at: indexPath) will return the existing cell.
Next, animating the setting of .isHidden won't give you the animation you want.
In DisclaimersCell.swift -> toggleExpansion(), change:
UIView.animate(withDuration: 0.25) { [unowned self] in
self.disclaimers.isHidden = !cellItem.isExpanded
}
to simply:
self.disclaimers.isHidden = !cellItem.isExpanded
Then, in your TableViewController didSelectRowAt function:
cellItem.isExpanded = !cellItem.isExpanded
cell.toggleExpansion(tableViewCellItem: cellItem)
tableView.beginUpdates()
tableView.endUpdates()
That will tell the cell to hide/show the label, and pairing .beginUpdates() with endUpdates() will trigger the recalculation of row heights in your table, and animate the redraw.

iOS - How to add bubble with text/buttons over cell in UITableView?

I really have no idea how to call this feature, music app had it up to 8.4, it looks like on the screenshot. I want to implement it in my app so when user presses the cell the "bubble" with 2 option buttons shows up.
I am interested in how to make it happen in Obj-C but I'm sure people will apreciate the answer written in Swift. Thanks
Screenshot
I'm doing exactly the same thing as what you want.
To do so, you have to create your own view controller, not UIMenuItem. The screen capture is as follows.
What I'm doing is to create a viewController as popup (PopoverMenuController), and adding a tableView as a subView for menu.
In the same way, you can add whatever UI controls you want instead of tableView.
Here is how you use my PopoverMenuController.
var myPopupMenu: PopoverMenuController!
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// make cell fully visible
let tableCell = tableView.cellForRow(at: indexPath)!
tableView.scrollRectToVisible(tableView.rectForRow(at: indexPath), animated: false)
// pop up menu
myPopupMenu = PopoverMenuController()
myPopupMenu.sourceView = tableCell
var rect = tableCell.bounds
// I'm adjusting popup menu position slightly (this is optional)
rect.origin.y = rect.size.height/3.0
rect.size.height = rect.size.height / 2.0
myPopupMenu.sourceRect = rect
myPopupMenu.addAction(PopMenuAction(textLabel: "MyMenu-1", accessoryType: .none, handler: myMenuHandler1))
myPopupMenu.addAction(PopMenuAction(textLabel: "MyMenu-2", accessoryType: .none, handler: myMenuHandler2))
present(myPopupMenu, animated: true, completion: nil)
}
func myMenuHandler1(_ action: PopMenuAction?) -> Bool {
// do some work...
return false
}
func myMenuHandler2(_ action: PopMenuAction?) -> Bool {
// do some work...
return false
}
To transltae to Obj-C should not be a big effort.
I put souce code of PopoverMenuController.swift here. This controller is self-contained and it has a description on how to use.
Hope this helps.
Look up how to implement UIMenuController on a UITableViewCell
How to show a custom UIMenuItem for a UITableViewCell

tvos UICollectionView lose focus to previous Focus Cell

I am building a tvos application. i have a strange bug where UICollectionView lose focus of the previously selected cell when i navigate back to that particular view. The scenario is some thing this like this.
I have two UIViewControllers A and B. A has a UITableView and it has three prototype cells in it. Each cell has a horizontal scrolling UICollectionView inside it. When i click on any of UICollectionViewCell it navigates to the B (detail page). I am presenting B modally.
Now when i press Menu button on Siri remote view A appears again (in other words view B is removed from View hierarchy) but the current selected cell is different then the previously selected. I have tried to use remembersLastFocusedIndexPath with both true and false values and also tried by implementing
func indexPathForPreferredFocusedViewInCollectionView(collectionView: UICollectionView) -> NSIndexPath?
but the control neves comes to this function when i navigate back to view A. I am also reloading every thing in viewWillAppear function.
Can any one help me in this. Thanks
The property remembersLastFocusedIndexPath should be set to true for the collectionView and false for the tableView.
Also, Are you reloading the UITableView in viewWillAppear i.e Is the table data being refreshed when the B is popped and A appears?
If Yes, then lastFocusedIndexPath will be nil on reload.
We faced the same issue. We solved it by not reloading the contents when B is popped.
Maintain a flag say didPush. Set this flag to true when B is pushed. When A appears check whether the flag is set and only then fetch data and reload table.
This worked for us.
I don't remember exactly, but I know there was a known issue for remembersLastFocusedIndexPath where it wasn't working as intended.
This is one workaround, although take it with a grand of salt as it does seem slightly hacky and it uses the common (but potentially unstable) approach of overriding the preferredFocusedView property.
private var viewToFocus: UIView?
override var preferredFocusView: UIView? {
get {
return self.viewToFocus
}
}
Save locally the indexPath of the last cell in View A when presenting View B
// [1] Saving and scrolling to the correct indexPath:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
...
collectionView.scrollToItemAtIndexPath(indexPath:, atScrollPosition:, animated:)
}
// [2] Create a dispatchTime using GCD before setting the cell at indexPath as the preferredFocusView:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
...
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(CGFloat.min * CGFloat(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.viewToFocus = collectionView.cellForItemAtIndexPath(indexPath:)
// [3] Request an update
self.setNeedsFocusUpdate()
// [4] Force a focus update
self.updateFocusIfNeeded()
}
}
The reason we split the two methods into both viewWillAppear and viewDidAppear is that it eliminates a bit of the animation jump. If anyone else could jump in with suggestions to improve or even alternate solutions, I'd also be interested!
In Swift
If you want to focus collection view Cell then
You can use collectionview Delegate method, method name is
func collectionView(collectionView: UICollectionView, didUpdateFocusInContext context: UICollectionViewFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator)
{
}
You can use this method like this...
func collectionView(collectionView: UICollectionView, didUpdateFocusInContext context: UICollectionViewFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = collectionView.cellForItemAtIndexPath(previousIndexPath) {
cell.contentView.layer.borderWidth = 0.0
cell.contentView.layer.shadowRadius = 0.0
cell.contentView.layer.shadowOpacity = 0
}
if let indexPath = context.nextFocusedIndexPath,
let cell = collectionView.cellForItemAtIndexPath(indexPath) {
cell.contentView.layer.borderWidth = 8.0
cell.contentView.layer.borderColor = UIColor.blackColor().CGColor
cell.contentView.layer.shadowColor = UIColor.blackColor().CGColor
cell.contentView.layer.shadowRadius = 10.0
cell.contentView.layer.shadowOpacity = 0.9
cell.contentView.layer.shadowOffset = CGSize(width: 0, height: 0)
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: [.CenteredHorizontally, .CenteredVertically], animated: true)
}
}

Resources