I have a UITableView with more cells than fit on screen. When I get a notification from my data model I want to jump to a specific row and show a very basic animation.
My code is:
func animateBackgroundColor(indexPath: NSIndexPath) {
dispatch_async(dispatch_get_main_queue()) {
NSLog("table should be at the right position")
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? BasicCardCell {
var actColor = cell.backgroundColor
self.manager.vibrate()
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = UIColor.redColor() }, completion: {
_ in
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = actColor }, completion: { _ in
self.readNotificationCount--
if self.readNotificationCount >= 0 {
var legicCard = self.legicCards[indexPath.section]
legicCard.wasRead = false
self.reloadTableViewData()
} else {
self.animateBackgroundColor(indexPath)
}
})
})
}
}
}
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int {
dispatch_sync(dispatch_get_main_queue()){
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: index), atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
self.animateBackgroundColor(NSIndexPath(forRow: 0, inSection: index))
}
}
I hoped that the dispatch_sync part would delay the execution of my animateBackgroundColor method until the scrolling is done. Unfortunately that is not the case so that animateBackgroundColor gets called when the row is not visible yet -> cellForRowAtIndexPath returns nil and my animation won't happen. If no scrolling is needed the animation works without problem.
Can anyone tell my how to delay the execution of my animateBackgroundColor function until the scrolling is done?
Thank you very much and kind regards
Delaying animation does not seem to be a good solution for this since scrollToRowAtIndexPath animation duration is set based on distance from current list item to specified item. To solve this you need to execute animateBackgroudColor after scrollToRowAtIndexPath animation is completed by implementing scrollViewDidEndScrollingAnimation UITableViewDelegate method. The tricky part here is to get indexPath at which tableview did scroll. A possible workaround:
var indexPath:NSIndexpath?
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int{
dispatch_sync(dispatch_get_main_queue()){
self.indexPath = NSIndexPath(forRow: 0, inSection: index)
self.tableView.scrollToRowAtIndexPath(self.indexPath, atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
}
}
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
self.animateBackgroundColor(self.indexPath)
indexPath = nil
}
Here are my solution
1) Create a .swift file, and copy code below into it:
typealias SwagScrollCallback = (_ finish: Bool) -> Void
class UICollectionViewBase: NSObject, UICollectionViewDelegate {
static var shared = UICollectionViewBase()
var tempDelegate: UIScrollViewDelegate?
var callback: SwagScrollCallback?
func startCheckScrollAnimation(scroll: UIScrollView, callback: SwagScrollCallback?){
if let dele = scroll.delegate {
self.tempDelegate = dele
}
self.callback = callback
scroll.delegate = self
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
callback?(true)
if let dele = self.tempDelegate {
scrollView.delegate = dele
tempDelegate = nil
}
}
}
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, _ callback: SwagScrollCallback?){
UICollectionViewBase.shared.startCheckScrollAnimation(scroll: self, callback: callback)
self.scrollToItem(at: indexPath, at: scrollPosition, animated: true)
}
}
2) Example:
#IBAction func onBtnShow(){
let index = IndexPath.init(item: 58, section: 0)
self.clv.scrollToItem(at: index, at: .centeredVertically) { [weak self] (finish) in
guard let `self` = self else { return }
// Change color temporarily
if let cell = self.clv.cellForItem(at: index) as? MyCell {
cell.backgroundColor = .yellow
cell.lbl.textColor = .red
}
// Reset
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.clv.reloadData()
}
}
}
3) My github code example: github here
I has similar problem. I just do this.
UIView.animateWithDuration(0.2,delay:0.0,options: nil,animations:{
self.tableView.scrollToRowAtIndexPath(self.indexPath!, atScrollPosition: .Middle, animated: true)},
completion: { finished in UIView.animateWithDuration(0.5, animations:{
self.animateBackgroundColor(self.indexPath)})})}
Related
I have a tableView in my view controller and I have it such that the tableView shrinks and expands depending on whether the keyboard is on the screen or off the screen. When the tableView shrinks, I want it to scroll to the bottom row but I can't get this to work.
Currently, I have the following:
#objc func keyboardAppears(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
bottomConstraint.constant = -1 * keyboardFrame.height
view.loadViewIfNeeded()
tableView.scrollToRow(at: IndexPath(row: 10, section: 0), at: .top, animated: true)
}
}
I also tried putting it in the main thread. This got it to work the first time the keyboard went up only. The code looked as follows:
#objc func keyboardAppears(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
bottomConstraint.constant = -1 * keyboardFrame.height
view.loadViewIfNeeded()
parentView.scrollUp()
}
}
func scrollUp() {
DispatchQueue.main.async {
self.entrySpace.scrollToRow(at: IndexPath(row: self.data.count - 1, section: 0), at: .top, animated: true)
}
}
UPDATE:
The following works after a letter is typed in the field (I want it to work right when the keyboard comes up)
NOTE: this is the UITableViewCell class because the cell is the last item in the UITableView
import UIKit
class EntryButtonCell: UITableViewCell {
//the textView for new entries
var entryCell = UITextView()
//Reference to the parent table
var parentTable = UITableView()
//Reference to the parent view (the one that holds the table view)
var parentView = ParentVC()
var bottomConstraint = NSLayoutConstraint()
//Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
//set this cell as the delegate for the entry textView
entryCell.delegate = self
//deactivate interaction with the cell so the user can interact with the textView
contentView.isUserInteractionEnabled = false
//set up the textView
prepTextView()
//set the notification for the keyboard events
setKeyboardObservers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//deinitialization
deinit {
//remove the keyboard observers
removeKeyboardObservers()
}
func prepTextView() {
//just did the constraints here
//removing to save space
}
func scrollToBottom() {
DispatchQueue.main.async { [weak self] in
guard let data = self?.parentView.data else { return }
let indexPath = IndexPath(row: data.count, section: 0)
self?.parentTable.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
//set the keyboard notification
private func setKeyboardObservers() {
//add keyboardWillShowNotification
NotificationCenter.default.addObserver(self, selector: #selector(keyboardAppears(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
//add keyboardWillHideNotification
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDisappears(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
//deactivate the keyboard notifications
private func removeKeyboardObservers() {
//deactivate keyboardWillShowNotification
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
//deactivate keyboardWillHideNotification
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
//run when keyboard shows
#objc func keyboardAppears(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
scrollToBottom()
bottomConstraint.constant = -1 * keyboardFrame.height
parentTable.layoutSubviews()
parentView.loadViewIfNeeded()
}
}
#objc func keyboardDisappears(notification: Notification) {
tableBottomConstraint.constant = -100.0
parentView.loadViewIfNeeded()
}
//Took out a few of the textView methods to save space
}
Swift 5
I have tested the following code based on your logic so that you just add a few lines to get it to work.
// MARK: Keyboard Handling
#objc func keyboardWillShow(notification: Notification)
{
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
{
tableView.reloadData()
scrollToBottom()
tableBottomConstraint.constant = -1 * (keyboardFrame.height + 100)
loadViewIfNeeded()
}
}
#objc func keyboardWillHide(notification: Notification)
{
tableBottomConstraint.constant = -100.0
loadViewIfNeeded()
}
func scrollToBottom()
{
DispatchQueue.main.async { [weak self] in
guard let data = self?.data else { return }
let indexPath = IndexPath(row: data.count - 1, section: 0)
self?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
Result
The output of my code will be something like this:
After updating a constraint, you have to commit the update by calling the layoutSubview() and after that you can call the scrollToRow(at:at:animated) method.
#objc func keyboardAppears(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
bottomConstraint.constant = -keyboardFrame.height
view.layoutSubviews()
self.entrySpace.scrollToRow(at: IndexPath(row: self.data.count - 1, section: 0), at: .top, animated: true)
}
}
According to https://developer.dji.com/api-reference/ios-uilib-api/Widgets/PreFlightStatusWidget.html:
"Tapping on status text will toggle between show and hide DUXPreflightChecklistController."
When I tap on the status text in the widget, the DUXPreflightChecklistController is not shown. Also, if I manually show the DUXPreflightChecklistController, there is a close button in the top right corner of the panel but tapping it does not hide the panel.
What is the proper way to configure this panel?
I'm using DJISDK 4.7.1 and DJIUXSDK 4.7.1 with Swift and iOS 12/xCode 10.0.
To provide a bit more detail, I do not want to use the Default Layout but I am using DUXStatusBarViewController. That is embedded in a UIView across the top of my app. I cannot find any properties for that controller that would allow me to hook it up to my instance of DUXPreflightChecklistController, which is also embedded in a UIView.
For: DUXPreflightChecklistController
I'd just solved that
var preflightChecklistController: DUXPreflightChecklistController!
weak var preFlightTableView: UITableView!
private var compassItemIndex: Int = -1
private var storageItemIndex: Int = -1
override func viewDidLoad() {
super.viewDidLoad()
preflightChecklistController = DUXPreflightChecklistController()
addChild(preflightChecklistController)
}
func renderChecklist() {
if let checklistVC = preflightChecklistController {
for subview in checklistVC.view.subviews {
if subview.isKind(of: UITableView.self) {
if let tableView = subview as? UITableView {
self.view.addSubview(tableView)
preFlightTableView = tableView
}
}
}
guard let checklistManager = checklistVC.checklistManager else { return }
let itemList = checklistManager.preFlightChecklistItems
for (index, item) in itemList.enumerated() {
if let _ = item as? DUXStorageCapacityChecklistItem {
storageItemIndex = index
}
if let _ = item as? DUXCompassChecklistItem {
compassItemIndex = index
}
}
preFlightTableView.reloadData()
checklistManager.startCheckingList()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
renderChecklist()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard preFlightTableView != nil else { return }
if let compassCell = preFlightTableView.cellForRow(at: IndexPath(item: compassItemIndex, section: 0)) {
for view in compassCell.subviews {
if let button = view as? UIButton, button.titleLabel?.text == "Calibrate" {
button.addTarget(self, action: #selector(doActionForSpecifiedBTN(sender:)), for: .touchUpInside)
break
}
}
}
if let storageCell = preFlightTableView.cellForRow(at: IndexPath(item: storageItemIndex, section: 0)) {
for view in storageCell.subviews {
if let button = view as? UIButton, button.titleLabel?.text == "Format" {
button.addTarget(self, action: #selector(doActionForSpecifiedBTN(sender:)), for: .touchUpInside)
break
}
}
}
}
#objc func doActionForSpecifiedBTN(sender: UIButton) {
guard let btnTitle = sender.titleLabel else { return }
switch btnTitle.text {
case "Calibrate":
// your func goes here
case "Format":
// your func goes here
default:
break
}
}
I am displaying images in a collection view controller. When the cell is tapped, I am passing those images to page view controller, where the user is given an option to delete or add image description as you can see in the below images.
When the user clicks delete button, I would the page (or view controller) to be deleted (just like the behaviour seen, when delete button is clicked in in Apple iOS photos app).
I tried to achieve it, by passing an array of empty view controller to pageViewController (See Code A), which resulted in a error
The number of view controllers provided (0) doesn't match the number required (1) for the requested transition which makes sense.
Am I on the right track, if yes, how can I fix the issue ?
If not, Is there a better approach to achieve the same ?
Code A: Taken from Code B
pageVC.setViewControllers([], direction: .forward, animated: true, completion: nil)
Code B: Taken from UserPickedImageVC
func deleteCurrentImageObject(){
guard let controllers = self.navigationController?.viewControllers else{
return
}
for viewController in controllers {
if viewController.className == "UserPickedImagesVC"{
let vc = viewController as! UserPickedImagesVC
let objectCount = vc.imageObjectsArray.count
guard objectCount > 0 && objectCount >= itemIndex else {
return
}
vc.imageObjectsArray.remove(at: itemIndex) // Removing imageObject from the array
if let pageVC = vc.childViewControllers[0] as? UIPageViewController {
pageVC.setViewControllers([], direction: .forward, animated: true, completion: nil)
}
}
}
}
Storyboard
Here is the complete code (except some custom UICollectionViewCell):
UserPickedImagesCVC.swift
import UIKit
import ImagePicker
import Lightbox
private let imageCellId = "imageCell"
private let addCellId = "addImagesCell"
class UserPickedImagesCVC: UICollectionViewController, ImagePickerDelegate, UserPickedImagesVCProtocol {
let imagePickerController = ImagePickerController()
//var userPickedImages = [UIImage]()
var userPickedImages = [ImageObject]()
override func viewDidLoad() {
super.viewDidLoad()
imagePickerController.delegate = self as ImagePickerDelegate
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Register cell classes
self.collectionView!.register(ImageCVCell .self, forCellWithReuseIdentifier: imageCellId)
self.collectionView!.register(ImagePickerButtonCVCell.self, forCellWithReuseIdentifier: addCellId)
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return userPickedImages.count + 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = indexPath.item
print("item: \(item)")
if item < userPickedImages.count {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellId, for: indexPath) as! ImageCVCell
let userPickedImageObject = userPickedImages[item]
//cell.showImagesButton.setImage(userPickedImage, for: .normal)
cell.showImagesButton.setImage(userPickedImageObject.image, for: .normal)
cell.showImagesButton.addTarget(self, action: #selector(showAlreadyPickedImages), for: .touchUpInside)
//cell.addButton.addTarget(self, action: #selector(showAlreadyPickedImages), for: .touchUpInside)
return cell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: addCellId, for: indexPath) as! ImagePickerButtonCVCell
cell.addButton.addTarget(self, action: #selector(showImagePickerController), for: .touchUpInside)
return cell
// Configure the cell
}
//Function shows imagePicker that helps in picking images and capturing images with camera
func showImagePickerController(){
print("showImagePickerController func called")
//self.present(imagePickerController, animated: true, completion: nil)
self.navigationController?.pushViewController(imagePickerController, animated: true)
}
func showAlreadyPickedImages(){
let vc = self.storyboard?.instantiateViewController(withIdentifier: "userPickedImagesVC") as! UserPickedImagesVC
//vc.contentImages = userPickedImages
vc.imageObjectsArray = userPickedImages
vc.showingAlreadySavedImages = true
self.navigationController?.pushViewController(vc, animated: true)
}
func setImagesInCells(imageObjects : [ImageObject]){
print("setImagesInCells func called in CVC")
userPickedImages += imageObjects
collectionView?.reloadData()
}
// MARK: - ImagePickerDelegate
func cancelButtonDidPress(_ imagePicker: ImagePickerController) {
imagePicker.dismiss(animated: true, completion: nil)
}
func wrapperDidPress(_ imagePicker: ImagePickerController, images: [UIImage]) {
guard images.count > 0 else { return }
let lightboxImages = images.map {
return LightboxImage(image: $0)
}
let lightbox = LightboxController(images: lightboxImages, startIndex: 0)
imagePicker.present(lightbox, animated: true, completion: nil)
}
func doneButtonDidPress(_ imagePicker: ImagePickerController, images: [UIImage]) {
imagePicker.dismiss(animated: true, completion: nil)
let vc = storyboard?.instantiateViewController(withIdentifier: "userPickedImagesVC") as! UserPickedImagesVC
//vc.contentImages = images
vc.imageObjectsArray = convertImagesToImageObjects(images)
//self.present(vc, animated: true, completion: nil)
self.navigationController?.pushViewController(vc, animated: true)
}
func convertImagesToImageObjects(_ imagesArray : [UIImage]) -> [ImageObject]{
var imageObjects = [ImageObject]()
for image in imagesArray{
var imageObject = ImageObject()
imageObject.image = image
imageObject.imageDescription = ""
imageObjects.append(imageObject)
}
return imageObjects
}
}
UserPickedImagesVC.swift
import UIKit
protocol UserPickedImagesVCProtocol{
func setImagesInCells(imageObjects : [ImageObject])
}
class ImageObject : NSObject{
var imageDescription : String?
var image : UIImage?
}
class UserPickedImagesVC: UIViewController, UIPageViewControllerDataSource {
var pageViewController : UIPageViewController?
let placeholderText = "Image description.."
var imageObjectsArray = [ImageObject]()
var delegate : UserPickedImagesVCProtocol!
var showingAlreadySavedImages = false
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = [] // To avoid view going below nav bar
//self.delegate = self.navigationController?.viewControllers
// Do any additional setup after loading the view, typically from a nib.
if showingAlreadySavedImages{
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(doneTapped))
}else{
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(saveTapped))
}
// createImageAndDescriptionDict()
createPageViewController()
setupPageControl()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func createPageViewController(){
print("createPageViewController func called")
let pageController = self.storyboard?.instantiateViewController(withIdentifier: "userPickedImagesPageController") as! UIPageViewController
pageController.dataSource = self
if imageObjectsArray.count > 0 {
let firstController = getItemController(0)
let startingViewControllers = [firstController]
pageController.setViewControllers(startingViewControllers as! [UIViewController], direction: .forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview((pageViewController?.view)!)
pageViewController?.didMove(toParentViewController: self)
}
// Creata the appearance of pagecontrol
func setupPageControl(){
let appearance = UIPageControl.appearance()
appearance.pageIndicatorTintColor = UIColor.gray
appearance.currentPageIndicatorTintColor = UIColor.white
appearance.backgroundColor = UIColor.darkGray
}
//MARK: Delagate methods
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let itemController = viewController as! UserPickedImageVC
if itemController.itemIndex > 0 {
return self.getItemController(itemController.itemIndex-1)
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let itemController = viewController as! UserPickedImageVC
if itemController.itemIndex + 1 < imageObjectsArray.count{
return getItemController(itemController.itemIndex+1)
}
return nil
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return imageObjectsArray.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
func currentControllerIndex() -> Int{
let pageItemController = self.currentControllerIndex()
if let controller = pageItemController as? UserPickedImageVC{
return controller.itemIndex
}
return -1
}
func currentController() -> UIViewController?{
if(self.pageViewController?.viewControllers?.count)! > 0{
return self.pageViewController?.viewControllers?[0]
}
return nil
}
func getItemController(_ itemIndex:Int) -> UserPickedImageVC?{
if itemIndex < imageObjectsArray.count{
let pageItemController = self.storyboard?.instantiateViewController(withIdentifier: "userPickedImageVC") as! UserPickedImageVC
pageItemController.itemIndex = itemIndex
//pageItemController.imageName = imageObjectsArray[itemIndex]
//pageItemController.imageToShow = imageObjectsArray[itemIndex]
//pageItemController.imageToShow = getImageFromImageDescriptionArray(itemIndex, imagesAndDescriptionArray)
pageItemController.imageObject = imageObjectsArray[itemIndex]
pageItemController.itemIndex = itemIndex
pageItemController.showingAlreadySavedImage = showingAlreadySavedImages
print("Image Name from VC: \(imageObjectsArray[itemIndex])")
return pageItemController
}
return nil
}
// Passing images back to Collection View Controller when save button is tapped
func saveTapped(){
let viewControllers = self.navigationController?.viewControllers
//print("viewControllers: \(viewControllers)")
if let destinationVC = viewControllers?[0]{
self.delegate = destinationVC as! UserPickedImagesVCProtocol
//self.delegate.setImagesInCells(images : imageObjectsArray)
self.delegate.setImagesInCells(imageObjects : imageObjectsArray)
self.navigationController?.popToViewController(destinationVC, animated: true)
}
}
func doneTapped(){
let viewControllers = self.navigationController?.viewControllers
if let destinationVC = viewControllers?[0] {
self.navigationController?.popToViewController(destinationVC, animated: true)
}
}
}
UserPickedImageVC.swift
import UIKit
import ImageScrollView
extension UIViewController {
var className: String {
return NSStringFromClass(self.classForCoder).components(separatedBy: ".").last!;
}
}
class UserPickedImageVC: UIViewController, UITextViewDelegate {
var itemIndex : Int = 0
var imageDescription : String = ""
var imageScrollView = ImageScrollView()
var imageDescriptionTextView : UITextView!
var imageToShow : UIImage!
var imageObject : ImageObject?
var deleteButton = UIButton(type: .system)
var showingAlreadySavedImage = false
var pageViewController : UIPageViewController!
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = [] // To avoid images going below the navigation bars
pageViewController = self.parent as! UIPageViewController
setConstraints()
setImageAndDescription()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: TextView delegate methods
func textViewDidBeginEditing(_ textView: UITextView)
{
if (imageDescriptionTextView.text == "Image description..")
{
imageDescriptionTextView.text = ""
imageDescriptionTextView.textColor = .black
}
imageDescriptionTextView.becomeFirstResponder() //Optional
}
func textViewDidEndEditing(_ textView: UITextView)
{
let imageDescription = imageDescriptionTextView.text
if (imageDescription == "")
{
imageDescriptionTextView.text = "Image description.."
imageDescriptionTextView.textColor = .lightGray
}
imageObject?.imageDescription = imageDescription
updateImageObject(imageObject!)
imageDescriptionTextView.resignFirstResponder()
}
//MARK: Private Methods
func setImageAndDescription(){
if let imageToDisplay = imageObject?.image{
imageScrollView.display(image: imageToDisplay) // Setting Image
}
imageDescriptionTextView.text = imageObject?.imageDescription // Setting Description
}
// Function to update imageObject in UserPickedImagesVC
func updateImageObject(_ imageObject: ImageObject){
guard let controllers = self.navigationController?.viewControllers else{
return
}
for viewController in controllers {
if viewController.className == "UserPickedImagesVC" {
let vc = viewController as! UserPickedImagesVC
vc.imageObjectsArray[itemIndex] = imageObject
}
}
}
// Function to delete imageObject from UserPickedImagesVC
func deleteCurrentImageObject(){
guard let controllers = self.navigationController?.viewControllers else{
return
}
for viewController in controllers {
if viewController.className == "UserPickedImagesVC"{
let vc = viewController as! UserPickedImagesVC
let objectCount = vc.imageObjectsArray.count
guard objectCount > 0 && objectCount >= itemIndex else {
return
}
vc.imageObjectsArray.remove(at: itemIndex) // Removing imageObject from the array
if let pageVC = vc.childViewControllers[0] as? UIPageViewController {
pageVC.setViewControllers([], direction: .forward, animated: true, completion: nil)
}
}
}
}
func showOrHideDeleteButton(){
if showingAlreadySavedImage{
print("deleteButton.isNotHidden")
deleteButton.isHidden = false
}else{
print("deleteButton.isHidden")
deleteButton.isHidden = true
}
}
func setConstraints(){
let viewSize = self.view.frame.size
let viewWidth = viewSize.width
let viewHeight = viewSize.height
print("viewWidth: \(viewWidth), viewHeight: \(viewHeight)")
view.addSubview(imageScrollView)
imageScrollView.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
deleteButton.tintColor = Colors.iOSBlue
deleteButton.setImage(#imageLiteral(resourceName: "delete"), for: .normal)
deleteButton.backgroundColor = Colors.white
deleteButton.layer.cornerRadius = 25
deleteButton.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
//deleteButton.currentImage.
deleteButton.imageView?.tintColor = Colors.iOSBlue
deleteButton.addTarget(self, action: #selector(deleteCurrentImageObject), for: .touchUpInside)
deleteButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(deleteButton)
showOrHideDeleteButton()
imageDescriptionTextView = UITextView()
imageDescriptionTextView.delegate = self as! UITextViewDelegate
imageDescriptionTextView.text = "Image description.."
imageDescriptionTextView.textColor = .lightGray
//imageScrollView.clipsToBounds = true
imageDescriptionTextView.translatesAutoresizingMaskIntoConstraints = false
//imageDescriptionTextView.backgroundColor = UIColor.white.withAlphaComponent(0.8)
imageDescriptionTextView.backgroundColor = UIColor.white
imageDescriptionTextView.layer.cornerRadius = 5
imageDescriptionTextView.layer.borderColor = UIColor.lightGray.cgColor
imageDescriptionTextView.layer.borderWidth = 0.5
view.addSubview(imageDescriptionTextView)
let viewsDict = [
"imageDescriptionTextView" : imageDescriptionTextView,
"deleteButton" : deleteButton
] as [String:Any]
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-5-[imageDescriptionTextView]-70-|", options: [], metrics: nil, views: viewsDict))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[imageDescriptionTextView(50)]-5-|", options: [], metrics: nil, views: viewsDict))
imageDescriptionTextView.sizeThatFits(CGSize(width: imageDescriptionTextView.frame.size.width, height: imageDescriptionTextView.frame.size.height))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[deleteButton(50)]-5-|", options: [], metrics: nil, views: viewsDict))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[deleteButton(50)]-5-|", options: [], metrics: nil, views: viewsDict))
}
}
create a call back property in UserPickedImageVC.swift
typealias DeleteCallBack = (int) -> Void
...
var itemIndex : Int = 0
var deleteCallBack:DeleteCallBack?
...
func deleteCurrentImageObject(){
self.deleteCallBack?(self.itemIndex)
}
in UserPickedImagesVC.swift
func getItemController(_ itemIndex:Int) -> UserPickedImageVC?{
if itemIndex < imageObjectsArray.count{
let pageItemController = self.storyboard?.instantiateViewController(withIdentifier: "userPickedImageVC") as! UserPickedImageVC
pageItemController.itemIndex = itemIndex
pageItemController.imageObject = imageObjectsArray[itemIndex]
pageItemController.itemIndex = itemIndex
pageItemController.showingAlreadySavedImage = showingAlreadySavedImages
print("Image Name from VC: \(imageObjectsArray[itemIndex])")
pageItemController.deleteCallBack = {
[weak self] (index) -> Void in
self?.deleteItemAt(index: index)
}
return pageItemController
}
return nil
}
func deleteItemAt(index: Int) {
if (imageObjectsArray.count > 1) {
imageObjectsArray.remove(at: itemIndex)
self.pageViewController.dataSource = nil;
self.pageViewController.dataSource = self;
let firstController = getItemController(0)
let startingViewControllers = [firstController]
pageViewController.setViewControllers(startingViewControllers as! [UIViewController], direction: .forward, animated: false, completion: nil)
} else {
//redirect here
_ = navigationController?.popViewController(animated: true)
}
}
I am able to reorder my collectionView like so:
However, instead of all cells shifting horizontally, I would just like to swap with the following behavior (i.e. with less shuffling of cells):
I have been playing with the following delegate method:
func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath
however, I am unsure how I can achieve custom reordering behavior.
I managed to achieve this by creating a subclass of UICollectionView and adding custom handling to interactive movement. While looking at possible hints on how to solve your issue, I've found this tutorial : http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/.
The most important part there was that interactive reordering can be done not only on UICollectionViewController. The relevant code looks like this :
var longPressGesture : UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
// rest of setup
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.handleLongGesture(_:)))
self.collectionView?.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView?.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView?.endInteractiveMovement()
default:
collectionView?.cancelInteractiveMovement()
}
}
This needs to be inside your view controller in which your collection view is placed. I don't know if this will work with UICollectionViewController, some additional tinkering may be needed. What led me to subclassing UICollectionView was realisation that all other related classes/delegate methods are informed only about the first and last index paths (i.e. the source and destination), and there is no information about all the other cells that got rearranged, so It needed to be stopped at the core.
SwappingCollectionView.swift :
import UIKit
extension UIView {
func snapshot() -> UIImage {
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
extension CGPoint {
func distanceToPoint(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
}
}
struct SwapDescription : Hashable {
var firstItem : Int
var secondItem : Int
var hashValue: Int {
get {
return (firstItem * 10) + secondItem
}
}
}
func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}
class SwappingCollectionView: UICollectionView {
var interactiveIndexPath : NSIndexPath?
var interactiveView : UIView?
var interactiveCell : UICollectionViewCell?
var swapSet : Set<SwapDescription> = Set()
var previousPoint : CGPoint?
static let distanceDelta:CGFloat = 2 // adjust as needed
override func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool {
self.interactiveIndexPath = indexPath
self.interactiveCell = self.cellForItemAtIndexPath(indexPath)
self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
self.interactiveView?.frame = self.interactiveCell!.frame
self.addSubview(self.interactiveView!)
self.bringSubviewToFront(self.interactiveView!)
self.interactiveCell?.hidden = true
return true
}
override func updateInteractiveMovementTargetPosition(targetPosition: CGPoint) {
if (self.shouldSwap(targetPosition)) {
if let hoverIndexPath = self.indexPathForItemAtPoint(targetPosition), let interactiveIndexPath = self.interactiveIndexPath {
let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)
if (!self.swapSet.contains(swapDescription)) {
self.swapSet.insert(swapDescription)
self.performBatchUpdates({
self.moveItemAtIndexPath(interactiveIndexPath, toIndexPath: hoverIndexPath)
self.moveItemAtIndexPath(hoverIndexPath, toIndexPath: interactiveIndexPath)
}, completion: {(finished) in
self.swapSet.remove(swapDescription)
self.dataSource?.collectionView(self, moveItemAtIndexPath: interactiveIndexPath, toIndexPath: hoverIndexPath)
self.interactiveIndexPath = hoverIndexPath
})
}
}
}
self.interactiveView?.center = targetPosition
self.previousPoint = targetPosition
}
override func endInteractiveMovement() {
self.cleanup()
}
override func cancelInteractiveMovement() {
self.cleanup()
}
func cleanup() {
self.interactiveCell?.hidden = false
self.interactiveView?.removeFromSuperview()
self.interactiveView = nil
self.interactiveCell = nil
self.interactiveIndexPath = nil
self.previousPoint = nil
self.swapSet.removeAll()
}
func shouldSwap(newPoint: CGPoint) -> Bool {
if let previousPoint = self.previousPoint {
let distance = previousPoint.distanceToPoint(newPoint)
return distance < SwappingCollectionView.distanceDelta
}
return false
}
}
I do realize that there is a lot going on there, but I hope everything will be clear in a minute.
Extension on UIView with helper method to get a snapshot of a cell.
Extension on CGPoint with helper method to calculate distance between two points.
SwapDescription helper structure - it is needed to prevent multiple swaps of the same pair of items, which resulted in glitchy animations. Its hashValue method could be improved, but was good enough for the sake of this proof of concept.
beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool - the method called when the movement begins. Everything gets setup here. We get a snapshot of our cell and add it as a subview - this snapshot will be what the user actually drags on screen. The cell itself gets hidden. If you return false from this method, the interactive movement will not begin.
updateInteractiveMovementTargetPosition(targetPosition: CGPoint) - method called after each user movement, which is a lot. We check if the distance from previous point is small enough to swap items - this prevents issue when the user would drag fast across screen and multiple items would get swapped with non-obvious results. If the swap can happen, we check if it is already happening, and if not we swap two items.
endInteractiveMovement(), cancelInteractiveMovement(), cleanup() - after the movement ends, we need to restore our helpers to their default state.
shouldSwap(newPoint: CGPoint) -> Bool - helper method to check if the movement was small enough so we can swap cells.
This is the result it gives :
Let me know if this is what you needed and/or if you need me to clarify something.
Here is a demo project.
Swift 5 solution of #Losiowaty solution:
var longPressGesture : UILongPressGestureRecognizer!
override func viewDidLoad()
{
super.viewDidLoad()
// rest of setup
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture))
self.collectionView?.addGestureRecognizer(longPressGesture)
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer)
{
switch(gesture.state)
{
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
collectionView?.endInteractiveMovement()
default:
collectionView?.cancelInteractiveMovement()
}
}
import UIKit
extension UIView {
func snapshot() -> UIImage {
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
extension CGPoint {
func distanceToPoint(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - x), 2) + pow((p.y - y), 2))
}
}
struct SwapDescription : Hashable {
var firstItem : Int
var secondItem : Int
var hashValue: Int {
get {
return (firstItem * 10) + secondItem
}
}
}
func ==(lhs: SwapDescription, rhs: SwapDescription) -> Bool {
return lhs.firstItem == rhs.firstItem && lhs.secondItem == rhs.secondItem
}
class SwappingCollectionView: UICollectionView {
var interactiveIndexPath : IndexPath?
var interactiveView : UIView?
var interactiveCell : UICollectionViewCell?
var swapSet : Set<SwapDescription> = Set()
var previousPoint : CGPoint?
static let distanceDelta:CGFloat = 2 // adjust as needed
override func beginInteractiveMovementForItem(at indexPath: IndexPath) -> Bool
{
self.interactiveIndexPath = indexPath
self.interactiveCell = self.cellForItem(at: indexPath)
self.interactiveView = UIImageView(image: self.interactiveCell?.snapshot())
self.interactiveView?.frame = self.interactiveCell!.frame
self.addSubview(self.interactiveView!)
self.bringSubviewToFront(self.interactiveView!)
self.interactiveCell?.isHidden = true
return true
}
override func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint) {
if (self.shouldSwap(newPoint: targetPosition))
{
if let hoverIndexPath = self.indexPathForItem(at: targetPosition), let interactiveIndexPath = self.interactiveIndexPath {
let swapDescription = SwapDescription(firstItem: interactiveIndexPath.item, secondItem: hoverIndexPath.item)
if (!self.swapSet.contains(swapDescription)) {
self.swapSet.insert(swapDescription)
self.performBatchUpdates({
self.moveItem(at: interactiveIndexPath as IndexPath, to: hoverIndexPath)
self.moveItem(at: hoverIndexPath, to: interactiveIndexPath)
}, completion: {(finished) in
self.swapSet.remove(swapDescription)
self.dataSource?.collectionView?(self, moveItemAt: interactiveIndexPath, to: hoverIndexPath)
self.interactiveIndexPath = hoverIndexPath
})
}
}
}
self.interactiveView?.center = targetPosition
self.previousPoint = targetPosition
}
override func endInteractiveMovement() {
self.cleanup()
}
override func cancelInteractiveMovement() {
self.cleanup()
}
func cleanup() {
self.interactiveCell?.isHidden = false
self.interactiveView?.removeFromSuperview()
self.interactiveView = nil
self.interactiveCell = nil
self.interactiveIndexPath = nil
self.previousPoint = nil
self.swapSet.removeAll()
}
func shouldSwap(newPoint: CGPoint) -> Bool {
if let previousPoint = self.previousPoint {
let distance = previousPoint.distanceToPoint(p: newPoint)
return distance < SwappingCollectionView.distanceDelta
}
return false
}
}
if you want to track numbers of cells being dragged like this behaviour:
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let targetIndexPath = reorderCollectionView.indexPathForItem(at: gesture.location(in: reorderCollectionView)) else {
return
}
reorderCollectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
reorderCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: reorderCollectionView))
reorderCollectionView.performBatchUpdates {
self.reorderCollectionView.visibleCells.compactMap { $0 as? ReorderCell}
.enumerated()
.forEach { (index, cell) in
cell.countLabel.text = "\(index + 1)"
}
}
case .ended:
reorderCollectionView.endInteractiveMovement()
default:
reorderCollectionView.cancelInteractiveMovement()
}
}
Suspect I am doing something fundamentally wrong below... I have a horizontal collectionview and after dragging I want to snap the closest cell to the center. But my results are unpredictable... what am I doing wrong here?
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Turns out my original code was missing accounting for the collecitonview content offset. In addition, I've moved the centering into the scrollViewDidEndDecelerating callback. I've modified the original code and included it below.
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell] {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
Try this:
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:self.collectionView.center];
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
SWIFT:
let indexPath = self.collectionView.indexPathForItemAtPoint(self.collectionView.center)
self.collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
None of the solutions reliably worked for me, so I wrote this and it works 100% of the time. Very simple. Even the built in indexPathForItemAtPoint was not working 100%.
extension UICollectionView {
var centerMostCell:UICollectionViewCell? {
guard let superview = superview else { return nil }
let centerInWindow = superview.convert(center, to: nil)
guard visibleCells.count > 0 else { return nil }
var closestCell:UICollectionViewCell?
for cell in visibleCells {
guard let sv = cell.superview else { continue }
let cellFrameInWindow = sv.convert(cell.frame, to: nil)
if cellFrameInWindow.contains(centerInWindow) {
closestCell = cell
break
}
}
return closestCell
}
}
Updated version for Swift 4
var closestCell = collectionView.visibleCells[0]
for cell in collectionView.visibleCells {
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0 - collectionView.contentOffset.x)
if (cellDelta < closestCellDelta){
closestCell = cell
}
}
let indexPath = collectionView.indexPath(for: closestCell)
collectionView.scrollToItem(at: indexPath!, at: .centeredHorizontally, animated: true)
When stop dragging, just pick the center and use it to choose the indexpath
swift 4.2
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let indexPath = photoCollectionView.indexPathForItem(at: photoCollectionView.center)
photoCollectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .centeredHorizontally, animated: true)
}
var centerUICollectionViewCell: UICollectionViewCell? {
get {
// 1
guard let center = collectionView.superview?.convert(collectionView.center, to: collectionView) else { return nil }
// 2
guard let centerIndexPath = collectionView.indexPathForItem(at: center) else { return nil }
// 3
return collectionView.cellForItem(at: centerIndexPath)
}
}
1: Need to make sure we are converting the point from superview coordinate space to collectionView's space
2: Use the collectionView's local space point to find the indexPath
3: Return the cell for indexPath
public func closestIndexPathToCenter() -> IndexPath? {
guard let cv = collectionView else { return nil }
let visibleCenterPositionOfScrollView = Float(cv.contentOffset.x + (cv.bounds.size.width / 2))
var closestCellIndex = -1
var closestDistance: Float = .greatestFiniteMagnitude
for i in 0..<cv.visibleCells.count {
let cell = cv.visibleCells[i]
let cellWidth = cell.bounds.size.width
let cellCenter = Float(cell.frame.origin.x + cellWidth / 2)
// Now calculate closest cell
let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
if distance < closestDistance {
closestDistance = distance
closestCellIndex = cv.indexPath(for: cell)!.row
}
}
if closestCellIndex != -1 {
return IndexPath(row: closestCellIndex, section: 0)
}else {
return nil
}
}