How to check UITableViewCell fully visible / loaded with Swift - iOS (setSelected) - ios

I have UITableView with CustomCell. I want to rotate record image when a cell fully visible or visible more than half at least.
This code block in CustomTableViewCell.swift
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
RotateImage()
}
The problem is that image is starting rotate immediately custom cell appeared like image below.
Sample Image:
There are 2 cells. First one is fully visible and rotating. And second one is partially visible / loaded but its also rotating.
Is it possible to check visibility in setSelected code block or need to check with UITableView functions?
The result should be like that:
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
if fullyVisible == true
{
RotateImage()
}
}
Thanks.

If you want more control for the behavior of cells based on their visibility withing the bounds of your screen, you could use UIScrollViewDelegate functions
such as:
optional func scrollViewDidScroll(_ scrollView: UIScrollView)
Get an array of the tableView's visible cells.
you can check the bounds of each cell to see if it is on screen
Check if the cell's record is spinning and if not start the rotation.

I did something similar in one of my projects:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleArea = scrollView.contentOffset.y + scrollView.height
for cell in tableView.visibleCells {
guard let propertyGraphCell = cell as? PropertyGraphCell else { continue }
if cell.frame.midY <= visibleArea, let indexPath = tableView.indexPath(for: cell) {
switch visibleRows[indexPath.row] {
case .daysOnMarket:
if daysOnMarketAnimated {
propertyGraphCell.drawLine(animated: true)
daysOnMarketAnimated = false
}
case .priceTrend:
if priceTrendAnimated {
propertyGraphCell.drawLine(animated: true)
priceTrendAnimated = false
}
case .vendorDiscount:
if vendorDiscountAnimated {
propertyGraphCell.drawLine(animated: true)
vendorDiscountAnimated = false
}
case .marketInsights:
assert(false, "cell animation not supported")
}
}
}
}

Related

Programmatically turns the page, indicators does not change

I try the following approach found here
extension UIPageViewController {
func goToNextPage(){
guard let currentViewController = self.viewControllers?.first else { return }
guard let nextViewController = dataSource?.pageViewController( self, viewControllerAfter: currentViewController ) else { return }
setViewControllers([nextViewController], direction: .forward, animated: true, completion: nil)
}
}
It works, but there is one issue:
When the page is turned programatically, the indicator does not move. It seems that they move only when user turns. page with swipe
that's how indicators should look like after programmatic turn is performed:
instead they remain unchanged
Which leads to issue that hierarchy shown by indicators is rather [2,0,1] instead of [0,1,2]
This is how I implement indicators:
func presentationCount(for PageViewController:UIPageViewController) -> Int {
return 3
}
func presentationIndex(for PageViewController:UIPageViewController) -> Int {
return 0
}
How to make dots indicators move when the page is turned programatically?
Unfortunately you can't update UIPageControl embedded in UIPageViewController. However, you can have your own UIPageControl in UIPageViewController in order to get full control. Then you can update UIPageControl property programmatically upon updating your Page. Have a look at this article.
There is a workaround get the subviews of UIPageViewController, set the value to currentPage.
for subView in self.view.subviews{
if let pageControl = subView as? UIPageControl,
pageControl.numberOfPages > currentIndex{
pageControl.currentPage = currentIndex
}
}
You can, all you need to do is maintain the page index in a variable, let's call it currentPageIndex and use the following method:
// Part of UIPageViewControllerDelegate
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return currentPageIndex;
}
// In your Button action, set this variable
#IBAction func nextPage(_ sender: Any) {
var index = (pageViewController?.viewControllers?.last as! SinglePageViewController).pageIndex
currentPageIndex = index
}
That's it!! Your page indicator should work now.
I was stuck in a same situation as yours.

Disable temporarily forward swipe only in uiPageViewController

I'm writing a series of settings / setup screens using uiPageViewController (images at the bottom). The user configures stuff, and swipe to the next screen, and so on. But I would like to lock / disable / block the forward swipe until the settings has been accomplished by the user in the current screen.
I tried:
1) uiPageViewController.view.userInteractionEnabled = false. It blocks everything in the screen, including the backward swipe. I want to block only the forward.
2)
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if !forwardSwipeEnabled {
return nil
}
var index = contentViewControllers.indexOf(viewController)
index += 1
return contentViewControllers[index]
}
to return nil when forwardSwipeEnabled is set to false, and it works, but the forward swipe remains blocked even after I change forwardSwipeEnabled = true because UIPageViewController already called this function when the scene showed up on the screen.
If I go back and forward again, the function gets called again and it works.
----- edit -----
Calling pageViewController!.setViewControllers(viewControllers, direction: .Forward, animated: false, completion: nil) with the current view controller doesn't refresh or call the function again.
3) Only append the next screen (UIViewcontroller), after the user finishes, but the same problem as 2) occurs.
----- edit -----
4) I can get the gestureRecognizers with view.subviews.first?.gestureRecognizers, but setting them gestureRecognize.enabled = false blocks both forward and reverse. There aren't different gestureRecognizers for each to block selectively.
5) Intercepting the gesture and eating it when direction is forward or letting it run, with the bottom code when backward doesn't work because the user can start the swipe backward, trigger the function return and finish forward.
override func viewDidLoad() {
super.viewDidLoad()
for view: UIView in pageViewController!.view.subviews {
if (view is UIScrollView) {
let scrollView = (view as! UIScrollView)
self.scrollViewPanGestureRecognzier = UIPanGestureRecognizer()
self.scrollViewPanGestureRecognzier.delegate = self
scrollView.addGestureRecognizer(scrollViewPanGestureRecognzier)
}
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == scrollViewPanGestureRecognzier {
guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false }
let velocity = panGestureRecognizer.velocityInView(view)
let translation = panGestureRecognizer.translationInView(view)
if translation.x < 0 {
return true
}
if velocity.x < 0 {
return true
}
return false
}
return false
}
Example screen:
just as an suggestion you may reload datasource with setViewControllers:direction:animated:completion after your job is done. This should fix your next page.
Its obj-c code. it should has same code in swift.
Did you find the solution for these years?
I've found only one: immediately go back if forward is forbidden:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
let currentPage = orderedViewControllers!.index(of: pageContentViewController)!
if !nextButton.isEnabled && currentPage > pageControl.currentPage {
DispatchQueue.main.asyncAfter(deadline: .now()+0.05, execute: {
self.setViewControllers([self.orderedViewControllers![self.pageControl.currentPage]], direction: .reverse, animated: false, completion: nil)
})
}
}

UITableView doesn't shift when moving from textField to textField directly

I'm currently battling with the keyboard covering some textField issue on my Swift based iPhone app.
This image shows the primary problem (top three images show the problem, bottom three are what it should be doing):
When the textField in the tableViewCell is edited I want to move the whole tableview and navigation bar up. I can do this with the code below (snippets taken from the viewController):
var tableViewShiftedUp = false
...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! QuantityTableViewCell
let componentLocationQuantity = tableViewData[indexPath.row]
if let locationString = componentLocationQuantity.location.valueForKey("location_name") as? String {
cell.setCellContents(locationString, quantity: componentLocationQuantity.quantity.floatValue)
cell.quantityLabel.delegate = self
}
//get stock level related to current build rate and lead time (good - green, warning - orange, urgent - red)
let status = model.determineStockLevelStatus(component)
cell.setQuantityLabelBackgroundStatusColor(status)
if editingMode == true {
cell.makeQuantityEditable()
}
//add control event to detect text change
cell.quantityLabel.addTarget(self, action: #selector(quantityCellTextChanged(_:)), forControlEvents: UIControlEvents.EditingChanged)
cell.quantityLabel.addTarget(self, action: #selector(quantityCellSelected(_:)), forControlEvents: UIControlEvents.EditingDidBegin)
return cell
}
...
//MARK: - UITextfield delegate
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.view.endEditing(true)
if tableViewShiftedUp == true {
moveTable()
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func quantityCellSelected(textField: UITextField){
if tableViewShiftedUp == false {
moveTable()
}
//check table was moved
print(tableView.frame.origin.y)
}
func hideKeyboard(){
self.view.endEditing(true)
if tableViewShiftedUp == true {
moveTable()
}
}
func moveTable(){
if tableViewShiftedUp == true {
animateTableViewMoving(false, moveValue: 250)
animateNavigationBarMoving(false, moveValue: 250)
tableViewShiftedUp = false
} else {
animateTableViewMoving(true, moveValue: 250)
animateNavigationBarMoving(true, moveValue: 250)
tableViewShiftedUp = true
}
}
// moving the tableView
func animateTableViewMoving (up:Bool, moveValue :CGFloat){
let movementDuration:NSTimeInterval = 0.0
let movement:CGFloat = ( up ? -moveValue : moveValue)
UIView.beginAnimations( "animateView", context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(movementDuration )
self.tableView.frame = CGRectOffset(self.tableView.frame, 0, movement)
UIView.commitAnimations()
}
// moving the navigationBar
func animateNavigationBarMoving (up:Bool, moveValue :CGFloat){
let movementDuration:NSTimeInterval = 0.0
let movement:CGFloat = ( up ? -moveValue : moveValue)
UIView.beginAnimations( "animateView", context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(movementDuration )
self.midNavigationBar.frame = CGRectOffset(self.midNavigationBar.frame, 0, movement)
UIView.commitAnimations()
}
And it works fine when moving directly to the textField in the tableView. However, when I am already editing a textField outside the tableView the shift up doesn't happen and so the shifting toggle gets out of sync.
I've printed the tableView frame origin:
//check table was moved
print(tableView.frame.origin.y)
so I can see what the tableView is set to and on that first move from textField outside the tableView to textField inside the tableView, this property is what I would expect it to be 134, however it's still at 384 on the screen which it prints the next time it's called.
The problem doesn't occur when moving within cells in the tableView.
It feels like I've got some kind of race condition problem or I'm missing some delegate method being called that is throwing everything off when transitioning from one cell to the next.
Help would be great.
Check out this library: https://github.com/michaeltyson/TPKeyboardAvoiding.
Designed so that it gets the keyboard out of the way of text fields.
When you use autolayout you cannot resize frame directly like you doing, you must work with constraints (you can create IBOutlets for constraints created in IB or add new ones, then send layoutIfNeeded to your view from within an animation block).
You can follow official Apple instructions:
About Auto Layout and Layout Constraints
Animating Layer Content

issue while populating String into textView

uploaded video (7 sec) for describing my problem
i have a textView embed in a tableView cell when we enter something on that textView the textview automatically increases its height according to entered content and also cell increases its size for doing this am using this approach:-
//tableViewCell
/// Custom setter so we can initialise the height of the text view
var textString: String {
get {
return textView?.text ?? ""
}
set {
if let textView = textView {
textView.text = newValue
textViewDidChange(textView)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Disable scrolling inside the text view so we enlarge to fitted size
textView?.scrollEnabled = false
textView?.delegate = self
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
textView?.becomeFirstResponder()
} else {
textView?.resignFirstResponder()
}
}
}
extension MultiLineTextInputTableViewCell: UITextViewDelegate {
func textViewDidChange(textView: UITextView) {
let size = textView.bounds.size
let newSize = textView.sizeThatFits(CGSize(width: size.width, height: CGFloat.max))
// Resize the cell only when cell's size is changed
if size.height != newSize.height {
UIView.setAnimationsEnabled(false)
tableView?.beginUpdates()
tableView?.endUpdates()
UIView.setAnimationsEnabled(true)
if let thisIndexPath = tableView?.indexPathForCell(self) {
tableView?.scrollToRowAtIndexPath(thisIndexPath, atScrollPosition: .Bottom, animated: false)
}
}
}
here everything is fine but i added a function for Forwarding any content and for this am using this approach :-
forwardNoteAction = UITableViewRowAction(style: .Normal, title: "Forward"){ action, index in
let cell = tableView.cellForRowAtIndexPath(indexPath) as! NotesTableViewCell
if self.searchBar.text == "" {
let object: PFObject = self.noteObjects.objectAtIndex(indexPath.row) as! PFObject
let postsObjID = object.objectId!
let query = PFQuery(className: "Notes")
query.fromLocalDatastore()
query.getObjectInBackgroundWithId(postsObjID) {
(objectViewes, error) -> Void in
if error != nil {
print(error)
} else {
self.forwardObject = object
print(self.forwardObject)
//object.pinInBackground()
self.performSegueWithIdentifier("forwardNoteSegue", sender: nil)
self.tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
i am putting the content into a variable self.forwardObject and passing that variable with an segue forwardNoteSegue after passing self.forwardObject am using that content for filling my tableView (which is a form , consist of textViews and TextFields) but when am doing this my textView is not increasing its height according to content which is inside that textView ,
all i want is the textView should increase increase its height when we use "textView.text = copiedContent" same as when we enter anything into the textView manually
please for understanding my problem properly see the uploaded video (link is provided above)
if my question is not understandable than please let me know i'll fix it
thanks
You need to manually call textViewDidChange when you modify the content of you textview programatically.
This is documented in the UITextviewDelegate's description of the method:
The text view calls this method in response to user-initiated changes to the text. This method is not called in response to
programmatically initiated changes.

UITableView cell selection animation and NSFetchedResultsController

I have a UITableViewController which allows multiple selection and it presents model of the data stored in the CoreData.
When user taps on the row we need to animate it to selected state (change layout, fade in some elements etc). The problem is that when user taps on row and makes it selected - model gets updated (because we store selected items in the model too). Because of the NSFetchedResultController aborts our fancy animations as whole table gets reloaded.
To make it more clear. Here is configureCell method i call in tableView:cellForRowAtindexPath. The reason to have setSelected method with animated: false there is to set already selected cells to a proper state when user scrolls the table.
func configureCell(cell: MealCell, indexPath: NSIndexPath) {
let menuItem = self.fetchedResultsController.objectAtIndexPath(indexPath) as! MenuItem
cell.name = menuItem.name
// some cell initialisation
//we have this code to draw cells selected when user scrolls our table view. We don't need animation here.
if menuItem.isSelected {
cell.setSelected(true, animated: false)
} else {
cell.setSelected(false, animated: false)
}
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
}
All animations happen in setSelected method of the MealCell class
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
extraView.hidden = !selected
let extraViewAlpha: CGFloat = selected ? 1.0 : 0.0
self.extraViewWidthConstraint.constant = selected ? 38 : 0
if animated {
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
self.layoutIfNeeded()
}, completion: { completed -> Void in
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.extraView.alpha = extraViewAlpha
})
})
}
else {
self.layoutIfNeeded()
self.extraView.alpha = extraViewAlpha
}
}
The best is to change your way of storing selectedItems to prevent the fetchedResultsController from notifying the delegate of the changes. a workaround is to temporary set the fetchedResultsController.delegate to nil and set it back after animation ends.

Resources