如何实现交互式单元格滑动以触发过渡动画,如 WhatsApp
How to implement interactive cell swiping to trigger transitioning animation like WhatsApp
我正在寻找如何像 WhatsApp 单元格滑动一样实现,我已经使用 UIPanGestureRecognizer 实现了单元格滑动动画,唯一剩下的就是执行交互式动画-将新的 UIViewController
添加到 window 并根据手势识别器速度和 X 轴值显示它。
一些附加说明,以准确说明我想要实现的目标:
我有一个 UITableViewController
,里面有自定义的 UITableViewCell
。我希望能够从左向右拖动一个单元格以启动交互式动画。 (注意:我已经实现了单元格滑动)。
新的UIViewController
将从左向右推。
滑动单元格时,UITableViewController
的视图会向右移动,此时,我想显示它旁边的推动UIViewController
。
这里有一个 GIF,可以详细了解我需要的内容(GIF 是从右向左滑动单元格,我需要相反的方向):
我同意@DonMag,iOS slide menu
可能是您最好的选择。这是一个简单的示例:SimpleSideMenu
我建议使用 SWRevealViewController。使用他们的指南很容易设置,而且看起来非常棒。我发现,当您预加载下面显示的 UIViewController
时,效果会更好。
它为您正在寻找的功能增加了出色的用户体验。
如果您希望选择加入该功能,它也可以是用户交互的。我没有使用过交互功能,但它很容易上手 运行 只需几行代码:
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let mainVC = storyboard.instantiateInitialViewController()
let menuStoryboard = UIStoryboard(name: "Menu", bundle: sdkBundle)
let menuNav = menuStoryboard.instantiateInitialViewController() as! UINavigationController
let mainRevealVC = SWRevealViewController(rearViewController: menuNav, frontViewController: mainVC)
mainRevealVC?.modalTransitionStyle = .crossDissolve
present(mainRevealVC!, animated: true, completion: nil)
要显示 UIViewController
,您只需调用
// Every UIViewController will have a `self.revealViewController()` when you `import SWRevealViewController`
self.revealViewController().revealToggle(animated: true)
table 视图后面一定要有一个新的控制器吗?让我试着解释一下我在 WhatsApp 示例中的方法。让我们假设该应用程序有 ChatController
,它有 table 的聊天视图和滑动显示的 ChatDetailController
。
当您 select 对话时,不是呈现 ChatController
而是呈现 ChatParent
,这会自动创建并添加两个 children。 ChatController
和 ChatDetailController
。接下来用函数 cellDidSwipe(toPosition position: CGPoint)
定义一个名为 SwipeableCellDelegate
的协议,并使 ChatParent
符合它。当单元格被滑动时,parent 可以决定是否应该将聊天移开,如果是,那么移开多少。然后它可以简单地移动 ChatController
视图直接穿过它的 .view
属性,显示第二个 child,它后面的 ChatDetailController
。
与您发布的 gif 相比,这有两个缺点。
- 导航栏不会从聊天到聊天详细信息淡化。但是,我认为最好在动画完成时更新导航栏,至少我个人不喜欢这种淡入淡出的方式,您有时会看到两组导航项。我认为如果聊天在屏幕上,那么聊天项目应该存在,并且只有当详细视图完全显示时才应该更新项目。
- 第二件事是动画键盘关闭。我不知道如何更改键盘框架以使其与用户滚动的距离成比例地消失,但也许一旦检测到滑动它就会自动消失?这是许多应用程序的标准做法,因此它应该是一个不错的解决方案。
祝你好运!
有一个非常简单但非常适合您的情况的库,名为 SWNavigationController which implements just like UINavigationController's interactivePopGestureRecognizer also interactivePushGestureRecognizer. In your case you don't want the push to be triggered from UIScreenEdgePangesturerecognizer so you're better off customizing the implementation rather than installing the pod which is what I did. Here 您可以找到完整的简单项目,它可以满足您的要求。
我对 SWNavigationController 做了一些修改以支持用 UIPanGestureRecognizer 替换 UIScreenEdgePangesturerecognizer
import UIKit
// First in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let firstVc = ViewController()
let initialViewController: SWNavigationController = SWNavigationController(rootViewController: firstVc)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
// Your chat viewController
class ViewController: UIViewController {
var backgroundColors: [IndexPath : UIColor] = [ : ]
var swNavigationController: SWNavigationController {
return navigationController as! SWNavigationController
}
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.white
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "chat vc"
view.addSubview(collectionView)
let panGestureRecognizer: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(panGestureRecognizer)
// Replace navigation controller's interactivePushGestureRecognizer with our own pan recognizer.
// SWNavigationController uses uiscreenedgerecognizer by default which we don't need in our case.
swNavigationController.interactivePushGestureRecognizer = panGestureRecognizer
}
func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard fabs(recognizer.translation(in: collectionView).x) > fabs(recognizer.translation(in: collectionView).y) else {
return
}
// create the new view controller upon .began
if recognizer.state == .began {
// disable scrolling(optional)
collectionView.isScrollEnabled = false
// pan location
let location: CGPoint = recognizer.location(in: collectionView)
// get indexPath of cell where pan is taking place
if let panCellIndexPath: IndexPath = collectionView.indexPathForItem(at: location) {
// clear previously pushed viewControllers
swNavigationController.pushableViewControllers.removeAllObjects()
// create detail view controller for pan indexPath
let dvc = DetailViewController(indexPath: panCellIndexPath, backgroundColor: backgroundColors[panCellIndexPath]!)
swNavigationController.pushableViewControllers.add(dvc)
}
} else if recognizer.state != .changed {
collectionView.isScrollEnabled = true
}
// let navigation controller handle presenting
// (you can consume the initial pan translation on x axis to drag the cell to the left until a defined threshold and call handleRightSwipe: only after that)
swNavigationController.handleRightSwipe(recognizer)
}
}
// Cell detail view controller
class DetailViewController: UIViewController {
var indexPath: IndexPath
var backgroundColor: UIColor
init(indexPath: IndexPath, backgroundColor: UIColor) {
self.indexPath = indexPath
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "detail vc at: \(indexPath.row)"
view.backgroundColor = backgroundColor
}
}
我正在寻找如何像 WhatsApp 单元格滑动一样实现,我已经使用 UIPanGestureRecognizer 实现了单元格滑动动画,唯一剩下的就是执行交互式动画-将新的 UIViewController
添加到 window 并根据手势识别器速度和 X 轴值显示它。
一些附加说明,以准确说明我想要实现的目标:
我有一个
UITableViewController
,里面有自定义的UITableViewCell
。我希望能够从左向右拖动一个单元格以启动交互式动画。 (注意:我已经实现了单元格滑动)。新的
UIViewController
将从左向右推。滑动单元格时,
UITableViewController
的视图会向右移动,此时,我想显示它旁边的推动UIViewController
。
这里有一个 GIF,可以详细了解我需要的内容(GIF 是从右向左滑动单元格,我需要相反的方向):
我同意@DonMag,iOS slide menu
可能是您最好的选择。这是一个简单的示例:SimpleSideMenu
我建议使用 SWRevealViewController。使用他们的指南很容易设置,而且看起来非常棒。我发现,当您预加载下面显示的 UIViewController
时,效果会更好。
它为您正在寻找的功能增加了出色的用户体验。
如果您希望选择加入该功能,它也可以是用户交互的。我没有使用过交互功能,但它很容易上手 运行 只需几行代码:
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let mainVC = storyboard.instantiateInitialViewController()
let menuStoryboard = UIStoryboard(name: "Menu", bundle: sdkBundle)
let menuNav = menuStoryboard.instantiateInitialViewController() as! UINavigationController
let mainRevealVC = SWRevealViewController(rearViewController: menuNav, frontViewController: mainVC)
mainRevealVC?.modalTransitionStyle = .crossDissolve
present(mainRevealVC!, animated: true, completion: nil)
要显示 UIViewController
,您只需调用
// Every UIViewController will have a `self.revealViewController()` when you `import SWRevealViewController`
self.revealViewController().revealToggle(animated: true)
table 视图后面一定要有一个新的控制器吗?让我试着解释一下我在 WhatsApp 示例中的方法。让我们假设该应用程序有 ChatController
,它有 table 的聊天视图和滑动显示的 ChatDetailController
。
当您 select 对话时,不是呈现 ChatController
而是呈现 ChatParent
,这会自动创建并添加两个 children。 ChatController
和 ChatDetailController
。接下来用函数 cellDidSwipe(toPosition position: CGPoint)
定义一个名为 SwipeableCellDelegate
的协议,并使 ChatParent
符合它。当单元格被滑动时,parent 可以决定是否应该将聊天移开,如果是,那么移开多少。然后它可以简单地移动 ChatController
视图直接穿过它的 .view
属性,显示第二个 child,它后面的 ChatDetailController
。
与您发布的 gif 相比,这有两个缺点。
- 导航栏不会从聊天到聊天详细信息淡化。但是,我认为最好在动画完成时更新导航栏,至少我个人不喜欢这种淡入淡出的方式,您有时会看到两组导航项。我认为如果聊天在屏幕上,那么聊天项目应该存在,并且只有当详细视图完全显示时才应该更新项目。
- 第二件事是动画键盘关闭。我不知道如何更改键盘框架以使其与用户滚动的距离成比例地消失,但也许一旦检测到滑动它就会自动消失?这是许多应用程序的标准做法,因此它应该是一个不错的解决方案。
祝你好运!
有一个非常简单但非常适合您的情况的库,名为 SWNavigationController which implements just like UINavigationController's interactivePopGestureRecognizer also interactivePushGestureRecognizer. In your case you don't want the push to be triggered from UIScreenEdgePangesturerecognizer so you're better off customizing the implementation rather than installing the pod which is what I did. Here 您可以找到完整的简单项目,它可以满足您的要求。
我对 SWNavigationController 做了一些修改以支持用 UIPanGestureRecognizer 替换 UIScreenEdgePangesturerecognizer
import UIKit
// First in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let firstVc = ViewController()
let initialViewController: SWNavigationController = SWNavigationController(rootViewController: firstVc)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
// Your chat viewController
class ViewController: UIViewController {
var backgroundColors: [IndexPath : UIColor] = [ : ]
var swNavigationController: SWNavigationController {
return navigationController as! SWNavigationController
}
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.white
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "chat vc"
view.addSubview(collectionView)
let panGestureRecognizer: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(panGestureRecognizer)
// Replace navigation controller's interactivePushGestureRecognizer with our own pan recognizer.
// SWNavigationController uses uiscreenedgerecognizer by default which we don't need in our case.
swNavigationController.interactivePushGestureRecognizer = panGestureRecognizer
}
func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard fabs(recognizer.translation(in: collectionView).x) > fabs(recognizer.translation(in: collectionView).y) else {
return
}
// create the new view controller upon .began
if recognizer.state == .began {
// disable scrolling(optional)
collectionView.isScrollEnabled = false
// pan location
let location: CGPoint = recognizer.location(in: collectionView)
// get indexPath of cell where pan is taking place
if let panCellIndexPath: IndexPath = collectionView.indexPathForItem(at: location) {
// clear previously pushed viewControllers
swNavigationController.pushableViewControllers.removeAllObjects()
// create detail view controller for pan indexPath
let dvc = DetailViewController(indexPath: panCellIndexPath, backgroundColor: backgroundColors[panCellIndexPath]!)
swNavigationController.pushableViewControllers.add(dvc)
}
} else if recognizer.state != .changed {
collectionView.isScrollEnabled = true
}
// let navigation controller handle presenting
// (you can consume the initial pan translation on x axis to drag the cell to the left until a defined threshold and call handleRightSwipe: only after that)
swNavigationController.handleRightSwipe(recognizer)
}
}
// Cell detail view controller
class DetailViewController: UIViewController {
var indexPath: IndexPath
var backgroundColor: UIColor
init(indexPath: IndexPath, backgroundColor: UIColor) {
self.indexPath = indexPath
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "detail vc at: \(indexPath.row)"
view.backgroundColor = backgroundColor
}
}